The Wabe

Mind the toves. They can be a bit slithy. Don't even get me started on the borogoves.

Friday, February 1, 2013

Code for Triggerhappy

Here are the sketch files, four of them: triggerhappy, Display, Encoder, and MenuSystem

triggerhappy:


//triggerhappy by Karl Gruenewald (studiokpg@gmail.com)
/*Trigger generator for modular synthesizers.
 Uses internal or external clock
 Has 6 outputs (A-F) which can each be set to one of 3 kinds of output:
 Clicktable: read through a static rhythm pattern, offset and length can be changed
 Logic: choose from 7 logic modes based on any two other outputs
 Euclidean: generate an Euclidean rhythm pattern given total length and number of groups
 Save and load settings 0-9
 (Can play, but can't store tempos slower than 59 BPM)
 Swing, +/- 99%

 IMPORTANT: When putting this software on a new Arduino, you need to comment out the loadSettings()
 call in setup(). Otherwise, you will have garbage settings and things may not work. Save the default
 settings to slot 0, then you can re-enable the loadSettings() line.

 */

#include <SoftwareSerial.h>
#include <EEPROM.h>

//Using arrays as "clicktables" to store which clicks get a trigger (1=trigger, 0=no trigger)
//Put what ever you want in here!
// 1  2  3  4| 5  6  7  8| 9  10 11 12|13 14 15 16

boolean clickTables[16][16] = {
  {
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0      }
  ,//1
  {
    1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0      }
  ,//2
  {
    1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0      }
  ,//3
  {
    1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1      }
  ,//4
  {
    1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0      }
  ,//5
  {
    1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0      }
  ,//6
  {
    1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1      }
  ,//7
  {
    1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0      }
  ,//8
  {
    1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1      }
  ,//9
  {
    1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1      }
  ,//10
  {
    1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0      }
  ,//11
  {
    1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0      }
  ,//12
  {
    1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1      }
  ,//13
  {
    1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0      }
  ,//14
  {
    1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0      }
  ,//15
  {
    1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0      } //16
};

byte outParam[6][10]; //10 parameters for each of the 6 outputs:
/*
 0: Mode (0, clicktable, 1, logic or 2, Euclidean)
 1: Offset (128 = no offset)
 2: 0, Trigger or 1, Gate
 3: Clicktable number
 4: Length (if clicktable)
 5: Logic mode (1-7) 1= NOT; 2= AND; 3= OR; 4= NAND; 5= NOR; 6= XOR; 7= XNOR
 6: 1st Output to base logic on (Outputs must be lower number than the output doing the logic!)
 7: 2nd Output to base logic on
 8: Euclidean length (beats)
 9: Euclidean pulses (groups)
 */
byte paramA = 0; //variable for selecting output to work on

const int offsetRange = 16; //min and max offset amount
boolean logicStore[6][offsetRange]; //store logic values for delay

#define OUT_PORT PORTB //pins 8-13 as outputs
#define PORT_DIRECTION DDRB

byte nextGate = 0; //stores binary gates to write to OUT_PORT
unsigned long nextClock = 0; //when is next pulse
unsigned long nextOffClock = 0; //when to turn triggers off
const int trigLen = 20; //length of triggers
boolean noLow = false; //only turn gates off once per clock loop
byte offGate = 0; //for turning off triggers selectively

const int clockPin = A0; //input for external clock
int clockValue = 0; //variable to store value from clock input
boolean clockInternal = true; //select internal or external clock
boolean clockPrevious = false;
boolean clock = false; //it is a clock or not?
unsigned long lastClock = 0; //for external clock calculation
const int clockThreshold = 800;
boolean seqStop = false;
boolean extClock = false;

const int buttonPin = 7; //encoder button for menu system
boolean button1;
boolean b1Pushed = false;
boolean b1Previous = false;
unsigned long lastButtonTime = 0;
const int debounceTime = 20;

const int saveButtonPin = A1; //enter save/read mode
const int buttonThreshold = 400;
int saveRead = 255;
boolean saveButton = false;
int saveSlot = 0;
const int saveDebounce = 500;
unsigned long lastSave = 0;

const int syncButtonPin = A5; //synchronize/reset patterns
boolean syncButton = false;
int syncRead = 0;
const int syncDebounce = 500; //Keep sync from happening too often
unsigned long lastSync = 0;

const int syncIntExtPin = A3; //select internal/external sync
int sourceRead = 0; //temp variable for reading int/ext switch
unsigned long switchPrevious = 0;
const int switchThreshold = 400;

//Defaults
int rateMain = 250; //internal clock interval in ms
int swing = 0; //range -99 to 99 (percent)
boolean swingToggle = 0; //swing this beat or not
float swingMod = 0; //amount to add or subtract from the beat for swing
String BPM;
int clickStep[6] = {
  0,0,0,0,0,0}; //array to track which step each pattern is on
boolean outTemp[6] = {
  false, false, false, false, false, false}; //gather output values
boolean outPrev[6] = {
  false, false, false, false, false, false}; //keep track of current output status
const int BjorkLen = 32; //max length of Euclidean pattern
boolean rhythm[BjorkLen]; //array for Euclidean pattern

extern int encoderMode; //for menu system

volatile int8_t tmpdata = 0; //variable to pass encoder value

const int txPin = 5;  // serial output pin
const int rxPin = 4;  // unused, but SoftwareSerial needs it...

SoftwareSerial mySerial =  SoftwareSerial(rxPin, txPin);


void setup() {
  Serial.begin(9600);
  setupEncoder();
  setupDisplay();
  PORT_DIRECTION = B111111;
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);
  initParams();// initializes outParams array
  //IMPORTANT: comment out the following line first time you run this sketch!
  loadSettings(); //loads from save location 0 on startup (will load garbage until you do a save to slot 0!)
  pinMode(clockPin, INPUT);
  pinMode(saveButtonPin, INPUT);
  digitalWrite(saveButtonPin, HIGH);
  pinMode(syncButtonPin, INPUT);
  digitalWrite(syncButtonPin, HIGH);
  pinMode(syncIntExtPin, INPUT);
  digitalWrite(syncIntExtPin, HIGH);
  lastClock = millis();
}

void loop() {
  readInputs();
  if (tmpdata) {
    doUpdates();
    tmpdata = 0;
  }
  if (clockInternal){
    if (seqStop || extClock) {
      nextClock = millis() + rateMain;
      seqStop = false;
      extClock = false;
    }
    if ((millis() >= nextClock) ) {
      clockAction();
      nextOffClock = nextClock + trigLen; //time to make triggers go low
      swingMod = ((float)rateMain * ((float)swing / 100));
      if (swingToggle) nextClock = nextClock + rateMain + swingMod;
      else nextClock = nextClock + rateMain - swingMod;
      lastClock = millis();
      if (rateMain < 30) rateMain = 30;
      if (encoderMode == 1) {
        BPM = String(15000/rateMain);
        displayLED(BPM);
      }
      swingToggle = !swingToggle;
    }
  }

  else { //external clock
    extClock = true;
    if (clock) {
      clockAction();
      nextOffClock = nextClock + trigLen; //no swing with external clock
      nextClock = nextClock + rateMain;
      if (rateMain < 30) rateMain = 30;
      if (encoderMode == 1) {
        BPM = String(15000/rateMain);
        displayLED(BPM);
      }
    }
    if (seqStop) { //this value need to keep getting updated while seq is stopped
      nextOffClock = nextClock + trigLen;
    }
  }

  if ((millis() >= nextOffClock) && (noLow == false)) gatesLow();

  if (b1Pushed) {
    doButton();
    b1Pushed = false;
  }
  if (saveButton) {
    lastSave = millis();
    encoderMode = 14;
    displayLED("FILE");
  }
  if (syncButton) {
    lastSync = millis();
    synchronize();
  }
}


void readInputs() {

  //read encoder button
  button1 = digitalRead(buttonPin);
  if (button1) {
    if((b1Previous == false) && (millis() - lastButtonTime > debounceTime)) {
      b1Pushed = true;
      lastButtonTime = millis();
    }
    else b1Pushed = false;
    b1Previous = true;
  }
  else {
    b1Pushed = false;
    b1Previous = false;
  }

  //read file button
  saveRead = analogRead(saveButtonPin);
  if (saveRead < buttonThreshold) saveButton = false;
  else saveButton = true;
  if (saveButton && ((millis() - lastSave) > saveDebounce)) saveButton = true;
  else saveButton = false;

  //read sync button
  syncRead = analogRead(syncButtonPin);
  if (syncRead < buttonThreshold) syncButton = false;
  else syncButton = true;
  if (syncButton && ((millis() - lastSync) > syncDebounce)) syncButton = true;
  else syncButton = false;

  //read int/ext switch
  sourceRead = analogRead(syncIntExtPin);
  if (sourceRead > switchThreshold) clockInternal = true;
  else if (sourceRead <= buttonThreshold) clockInternal = false;

  //read clock pin
  if (!clockInternal) {
    clockValue = analogRead(clockPin);
    if (clockValue >= clockThreshold) {
      if (!clockPrevious) {
        clock = true;
        clockPrevious = true;
      }
      else clock = false;
    }
    else {
      clock = false;
      clockPrevious = false;
      if (millis() - lastClock > 2000) seqStop = true; //stop if no clock for 2 sec.
    }
    if (clock) {
      rateMain = millis() - lastClock;
      lastClock = millis();
      seqStop = false;
    }
  }
}


void clockAction() {
  noLow = false;
  for (int i = 0; i < 6; i++) { //go through each output A-F

    if (outParam[i][0] == 0) { //output is clicktable
      int length = outParam[i][4];
      int offset = outParam[i][1] - 128;
      if (abs(offset) > length) offset = offset % length;
      int counter = clickStep[i] + offset;
      if (counter > length - 1) counter = counter - length;
      if (counter < 0) counter = counter + length;
      int thisArray = outParam[i][3];
      outTemp[i] = clickTables[thisArray][counter];
      clickStep[i]++;
      if (clickStep[i] > length - 1) clickStep[i] = 0;
    }

    else if (outParam[i][0] == 1) { //output is logic;
      //NOTE: user must make sure logic outputs come after the ouputs they refer to, or logic will be based on previous beat
      byte baseA = outParam[i][6];//which output is base
      byte baseB = outParam[i][7];
      baseA = outTemp[baseA];//change base to value of click at that output
      baseB = outTemp[baseB];

      for (int j = offsetRange; j > 0; j--) { //Slide all the values down for delay table
        logicStore[i][j] = logicStore[i][j-1];
      }

      switch (outParam[i][5]) { //logic calculations

      case 0: //NOT
        logicStore[i][0] = !baseA;
        break;

      case 1: //AND
        logicStore[i][0] = baseA && baseB;
        break;

      case 2: //OR
        logicStore[i][0] = baseA || baseB;
        break;

      case 3: //NAND
        logicStore[i][0] = !(baseA && baseB);
        break;

      case 4: //NOR
        logicStore[i][0] = !(baseA || baseB);
        break;

      case 5: //XOR
        logicStore[i][0] = baseA ^ baseB;
        break;

      case 6: //XNOR
        logicStore[i][0] = !(baseA ^ baseB);
        break;
      }
      int offset = outParam[i][1] - 128;
      if (offset < 1) offset = 1;
      if (offset > offsetRange) offset = offsetRange;
      outTemp[i] = logicStore[i][offset - 1];
    }

    else {  //output is Euclidean
    // based on http://kreese.net/blog/2010/03/27/generating-musical-rhythms/
      int steps = outParam[i][8]; //beats
      int pulses = outParam[i][9]; //groups
      int pauses = steps - pulses;
      boolean switcher = false;
      if (pulses > pauses) {
        switcher = true;
        pauses ^= pulses;
        pulses ^= pauses;
        pauses ^= pulses;
      }
      int perPulse = floor(pauses / pulses);
      int remainder = pauses % pulses;
      int noSkip;
      if (remainder == 0) noSkip = 0;
      else noSkip = floor(pulses / remainder);
      int skipXTime;
      if (noSkip == 0) skipXTime = 0;
      else skipXTime = floor((pulses - remainder)/noSkip);

      int counter = 0;
      int skipper = 0;
      int pos = 0;
      for (int h = 1; h <= steps; h++) {
        if (counter == 0) {
          rhythm[pos] = !switcher;
          pos ++;
          counter = perPulse;

          if (remainder > 0 && skipper == 0) {
            counter++;
            remainder--;
            if (skipXTime > 0) skipper = noSkip;
            else skipper = 0;
            skipXTime--;
          }
          else {
            skipper --;
          }
        }
        else {
          rhythm[pos] = switcher;
          pos++;
          counter--;
        }
      }
         
      int c = clickStep[i] + outParam[i][1] - 128;
      c = abs(c % steps);
      outTemp[i] = rhythm[c];
      clickStep[i]++;
      if (clickStep[i] >= steps) clickStep[i] = 0;
    }
  }



  for (int k = 5; k > -1; k--) { //doing the output
    if (outParam[k][2]) {
      if (outTemp[k]) {
        outTemp[k] = !outPrev[k];
      }
      else outTemp[k] = outPrev[k];
    }
    nextGate = nextGate << 1;
    nextGate = nextGate | outTemp[k];
  }

  OUT_PORT = nextGate; //write gate outputs
  nextGate = 0;
  for (int k = 0; k < 6; k++) {
    outPrev[k] = outTemp[k];
  }
}

void gatesLow() {
  for (int k = 5; k > -1; k--) {
    offGate = offGate << 1;
    if (outParam[k][2] && outPrev[k]) {
      offGate = offGate | 1;
    }
  }
  OUT_PORT = offGate;
  offGate = 0;
  noLow = true;
}

void synchronize() { //resets all counters
  displayLED("SYNC");
  for (int i = 0; i < 6; i++) {
    clickStep[i] = 0;
  }
  swingToggle = 0;
  doUpdates();
}

void initParams() { //sets some default values for outParam
  for (int i = 0; i < 6; i++){
    for (int j = 0; j < 10; j++){
      outParam[i][j] = 0;
    }
  }
  for (int k = 0; k < 6; k++){
    outParam[k][1] = 128; //go back and set offsets to null value
  }
}

/*
 0: Mode (0, clicktable, 1, logic or 2, Euclidean)
 1: Offset (128 = no offset)
 2: 0, Trigger or 1, Gate
 3: Clicktable number
 4: Length (if clicktable)
 5: Logic mode (1-7) 1= NOT; 2= AND; 3= OR; 4= NAND; 5= NOR; 6= XOR; 7= XNOR
 6: 1st Output to base logic on
 7: 2nd Output to base logic on
 8: Euclidean length (beats)
 9: Euclidean pulses (groups)
 */

void saveSettings() { //will overwrite settings in EEPROM without warning!
  int slotByte = (saveSlot * 62); //62 paramaters to save, slotByte is first address per set
  int slotCounter = 0;
  for (int i = 0; i < 6; i++){
    for (int j = 0; j < 10; j++){
      EEPROM.write(slotByte + slotCounter, outParam[i][j]);
      slotCounter ++;
    }
  }
  EEPROM.write(slotByte + slotCounter, swing);
  slotCounter ++;
  int tempRate = (rateMain > 255) ? 255 : rateMain; //can't store more than 1 byte per EEPROM slot
  EEPROM.write(slotByte + slotCounter, tempRate); //rateMain 256 ~= 59 bpm
}


void loadSettings() {
  int slotByte = (saveSlot * 62); //62 paramaters to save, slotByte is first address per set
  int slotCounter = 0;
  for (int i = 0; i < 6; i++){
    for (int j = 0; j < 10; j++){
      outParam[i][j] = EEPROM.read(slotByte + slotCounter);
      slotCounter ++;
    }
  }
  swing = EEPROM.read(slotByte + slotCounter);
  slotCounter ++;
  rateMain = EEPROM.read(slotByte + slotCounter);
}

Display:


char charmessage[4];
int intmessage[4];

void setupDisplay()  {
  mySerial.begin(9600); // initialize communication to the display
  mySerial.write(0x7A); // command byte for brightness
  mySerial.write(0x01); // display brightness (lower = brighter)
}

void displayLED(String ledMessage)
{
  int strLength = ledMessage.length(); 
  if (strLength <= 4) {
    if (strLength < 4) mySerial.write(" ");
    if (strLength < 3) mySerial.write(" ");
    if (strLength < 2) mySerial.write(" ");
    for(int i = 0; i < strLength; i++){
      mySerial.write(ledMessage[i]);   
    }
  }
}


Encoder:

/*Rotary Encoder code from Oleg, Circuits@Home
http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino
plus interrupt code by "LEo" and "bikedude"
*/

#define ENC_A 2 //encoder pins
#define ENC_B 3
#define ENC_PORT PIND
int encoderCounter = 0; //variable for smoothing encoder response

void setupEncoder()
{
  /* Setup encoder pins as inputs */
  pinMode(ENC_A, INPUT);
  digitalWrite(ENC_A, HIGH);
  pinMode(ENC_B, INPUT);
  digitalWrite(ENC_B, HIGH);

  // encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoder, CHANGE);

  // encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoder, CHANGE);
}

void doEncoder()
{
  encoderCounter = encoderCounter + read_encoder();
  //make encoder less jumpy by waiting for 2 clicks in same direction!
  if (encoderCounter > 2) {
    tmpdata = 1;
    encoderCounter = 0;
  }
  if (encoderCounter < -2) {
    tmpdata = -1;
    encoderCounter = 0;
  }
}

/* returns change in encoder state (-1,0,1) */
int8_t read_encoder()
{
  static int8_t enc_states[] = {
    0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0    };
  static uint8_t old_AB = 0;
  uint8_t new_AB = ENC_PORT;

  new_AB >>= 2; //Shift the bits two positions to the right
  old_AB <<= 2;
  old_AB |= (new_AB & 0x03); //add current state
  return ( enc_states[( old_AB & 0x0f )]);
}




MenuSystem:

int mainSelector = 1; //top level
int encoderMode = 1; //menu location pointer
int mode = 0; //variable for output mode
int arrayNumber = 0; //variable for array number assignment
String tempString = ""; //variable to build display text
int arrayLength = 32; //variable to set array length
int patternOffset = 128; //variable to set pattern offset
boolean trigGate = true; //variable for trigger or gate selection
int logicMode = 0; //variable for logic mode selection
int logic1 = 0; //first logic input
int logic2 = 0; //second logic input
int EuclidBeats = 8; //Number of beats for Euclidean pattern
int EuclidGroups = 2; //Number of groups within Euclidean pattern
int saveFunction = 1; //Save or Load files

void doUpdates() //handles encoder input and display for menu system
{
  switch (encoderMode) {
 
    case 1: //Adjust Rate Main
      rateMain = rateMain - (tmpdata * rateMain/30); //make amount of change proportional to rate
      BPM = String(15000/rateMain);
      displayLED(BPM);
    break;
 
    case 2: //Output selector/Main Menu
      mainSelector = mainSelector + tmpdata;
      if (mainSelector > 8) mainSelector = 1;
      if (mainSelector < 1) mainSelector = 8;
      if (mainSelector == 1) displayLED("outA");
      else if (mainSelector == 2) displayLED("outB");
      else if (mainSelector == 3) displayLED("outC");
      else if (mainSelector == 4) displayLED("outD");
      else if (mainSelector == 5) displayLED("outE");
      else if (mainSelector == 6) displayLED("outF");
      else if (mainSelector == 7) displayLED("S3nG"); //swing ;)
      else if (mainSelector == 8) displayLED("RATE");
    break;
 
    case 3: //Select output mode
      mode = mode + tmpdata;
      if (mode > 2) mode = 0;
      if (mode < 0) mode = 2;
      if (mode == 0) displayLED("ARR ");
      else if (mode == 1) displayLED("LoG ");
      else if (mode == 2) displayLED("EuC ");
    break;
 
    case 4: //Select array number
      arrayNumber = arrayNumber + tmpdata;
      if (arrayNumber < 0) arrayNumber = 15;
      if (arrayNumber > 15) arrayNumber = 0;
      if (arrayNumber > 8) tempString = "AR";
      else tempString = "AR ";
      tempString += (arrayNumber + 1);
      displayLED(tempString);
      outParam[paramA][3] = arrayNumber;
    break;
 
    case 5: //Select array length
      arrayLength = arrayLength + tmpdata;
      if (arrayLength < 1) arrayLength = 1;
      if (arrayLength > 16) arrayLength = 16;
      if (arrayLength > 9) tempString = "AL";
      else tempString = "AL ";
      tempString += arrayLength;
      displayLED(tempString);
      outParam[paramA][4] = arrayLength;
    break;
 
    case 6: //Select pattern offset
      patternOffset = patternOffset + tmpdata;
      if (patternOffset < (128 - offsetRange)) patternOffset = (128 - offsetRange);
      if (patternOffset > (128 + offsetRange)) patternOffset = (128 + offsetRange);
      tempString = String(patternOffset - 128);
      displayLED(tempString);
      outParam[paramA][1] = patternOffset;
    break;
 
    case 7: //Select trigger or gate behavior
      trigGate = !trigGate;
      if (trigGate) displayLED("GATE");
      else displayLED("TRIG");
      outParam[paramA][2] = trigGate;
    break;
 
    case 8: //select logic mode
      logicMode = logicMode + tmpdata;
      if (logicMode < 0) logicMode = 6;
      if (logicMode > 6) logicMode = 0;
      if (logicMode == 0) displayLED("NoT ");
      else if (logicMode == 1) displayLED("AND ");
      else if (logicMode == 2) displayLED(" oR ");
      else if (logicMode == 3) displayLED("NAND");
      else if (logicMode == 4) displayLED("nor ");
      else if (logicMode == 5) displayLED("Hor ");
      else if (logicMode == 6) displayLED("Hnor");
      outParam[paramA][5] = logicMode;
    break;
 
    case 9: //select logic input 1
      logic1 = logic1 + tmpdata;
      if (logic1 < 0) logic1 = 5;
      if (logic1 > 5) logic1 = 0;
      if (logic1 == 0) displayLED("L1-A");
      else if (logic1 == 1) displayLED("L1-B");
      else if (logic1 == 2) displayLED("L1-C");
      else if (logic1 == 3) displayLED("L1-D");
      else if (logic1 == 4) displayLED("L1-E");
      else if (logic1 == 5) displayLED("L1-F");
      outParam[paramA][6] = logic1;
    break;
 
    case 10: //select logic input 2
      logic2 = logic2 + tmpdata;
      if (logic2 < 0) logic2 = 5;
      if (logic2 > 5) logic2 = 0;
      if (logic2 == 0) displayLED("L2-A");
      else if (logic2 == 1) displayLED("L2-B");
      else if (logic2 == 2) displayLED("L2-C");
      else if (logic2 == 3) displayLED("L2-D");
      else if (logic2 == 4) displayLED("L2-E");
      else if (logic2 == 5) displayLED("L2-F");
      outParam[paramA][7] = logic2;
    break;
 
    case 11: // + offset (delay)
      patternOffset = patternOffset + tmpdata;
      if (patternOffset < 128) patternOffset = 128;
      if (patternOffset > (128 + offsetRange)) patternOffset = (128 + offsetRange);
      tempString = String(patternOffset - 128) ;
      displayLED(tempString);
      outParam[paramA][1] = patternOffset;
    break;
 
    case 12: //Euclidean #beats
      EuclidBeats = EuclidBeats + tmpdata;
      if (EuclidBeats < 1) EuclidBeats = 1;
      if (EuclidBeats > 250) EuclidBeats = 250;
      if (EuclidBeats > 99) tempString = "b";
      else if (EuclidBeats > 9) tempString = "b ";
      else tempString = "b  ";
      tempString += EuclidBeats;
      displayLED(tempString);
      outParam[paramA][8] = EuclidBeats;
    break;
 
    case 13: //Euclidean #groups
      EuclidGroups = EuclidGroups + tmpdata;
      if (EuclidGroups < 1) EuclidGroups = 1;
      if (EuclidGroups > EuclidBeats) EuclidGroups = EuclidBeats;
      if (EuclidGroups > 99) tempString = "G";
      else if (EuclidGroups > 9) tempString = "G ";
      else tempString = "G  ";
      tempString += EuclidGroups;
      displayLED(tempString);
      outParam[paramA][9] = EuclidGroups;
    break;
 
    case 14: //Save & Read parameters from EEPROM
      saveFunction = saveFunction + tmpdata;
      if (saveFunction < 1) saveFunction = 3;
      if (saveFunction > 3) saveFunction = 1;
      if (saveFunction == 1) displayLED("SAUE");
      else if (saveFunction == 2) displayLED("LOAD");
      else displayLED("BACH"); //"back" ;)
    break;
 
    case 15: //Save settings
      saveSlot = saveSlot + tmpdata;
      if (saveSlot < 0) saveSlot = 0;
      if (saveSlot > 99) saveSlot = 99;
      if (saveSlot > 9) tempString = "S ";
      else tempString = "S  ";
      tempString += saveSlot;
      displayLED(tempString);
    break;
 
    case 16: //Load settings
      saveSlot = saveSlot + tmpdata;
      if (saveSlot < 0) saveSlot = 0;
      if (saveSlot > 99) saveSlot = 99;
      if (saveSlot > 9) tempString = "L ";
      else tempString = "L  ";
      tempString += saveSlot;
      displayLED(tempString);
    break;
 
    case 17: //Adjust swing
      swing = swing + tmpdata;
      if (swing < -99) swing = -99;
      if (swing > 99) swing = 99;
      if (swing < 0) tempString = "-";
      else tempString = "";
      tempString += abs(swing);
      displayLED(tempString);
    break;
  }
}

void doButton() //handles button pushes for menu system
{
  switch (encoderMode) {

  case 1: //Adjust Rate Main
      displayLED("out ");
      encoderMode = 2; //Go to Main Menu
      if (mainSelector == 1) displayLED("outA");
      else if (mainSelector == 2) displayLED("outB");
      else if (mainSelector == 3) displayLED("outC");
      else if (mainSelector == 4) displayLED("outD");
      else if (mainSelector == 5) displayLED("outE");
      else if (mainSelector == 6) displayLED("outF");
      else if (mainSelector == 7) displayLED("S3nG"); //swing ;)
      else if (mainSelector == 8) displayLED("RATE");
    break;

  case 2: //Output selector/Main Menu
      if (mainSelector < 7) {
        paramA = mainSelector - 1;
        encoderMode = 3;
     
        mode = outParam[paramA][0];
        if (mode == 0) displayLED("ARR ");
        else if (mode == 1) displayLED("LoG ");
        else if (mode == 2) displayLED("EuC ");
      }

      else if (mainSelector == 7) {
        encoderMode = 17; //Adjust swing
        if (swing < 0) tempString = "-";
        else tempString = "";
        tempString += abs(swing);
        displayLED(tempString);
      }
   
      else if (mainSelector == 8) {
        encoderMode = 1; //Go back to main rate
      }
    break;

  case 3: //Set output mode
      outParam[paramA][0] = mode;
      if (mode == 0) { //arrays
        encoderMode = 4;
        arrayNumber = outParam[paramA][3];
        if (arrayNumber > 8) tempString = "AR";
        else tempString = "AR ";
        tempString += (arrayNumber + 1);
        displayLED(tempString);
      }
      else if (mode == 1) { //logic
        encoderMode = 8;
        logicMode = outParam[paramA][5];
        if (logicMode == 0) displayLED("NoT ");
        else if (logicMode == 1) displayLED("AND ");
        else if (logicMode == 2) displayLED(" oR ");
        else if (logicMode == 3) displayLED("NAND");
        else if (logicMode == 4) displayLED("nor ");
        else if (logicMode == 5) displayLED("Hor ");
        else if (logicMode == 6) displayLED("Hnor");
      }
   
      else if (mode == 2) { //euclidean
        encoderMode = 12;
        if (EuclidBeats > 99) tempString = "b";
        else if (EuclidBeats > 9) tempString = "b ";
        else tempString = "b  ";
        tempString += EuclidBeats;
        displayLED(tempString);
      }

    break;

  case 4: //Set array number
      encoderMode = 5; //setting is done in doUpdates() to hear result right away
      arrayLength = outParam[paramA][4];
      if (arrayLength > 9) tempString = "AL";
      else tempString = "AL ";
      tempString += arrayLength;
      displayLED(tempString);
    break;

  case 5: //Set array length
      encoderMode = 6;
      patternOffset = outParam[paramA][1];
      tempString = String(patternOffset - 128);
      displayLED(tempString);
    break;

  case 6: //Set pattern offset
      encoderMode = 7;
      trigGate = outParam[paramA][2];
      if (trigGate) displayLED("GATE");
      else displayLED("TRIG");
    break;

  case 7: //Select trigger or gate
      encoderMode = 1; //go back to rate
      BPM = String(15000/rateMain);
      displayLED(BPM);
    break;

  case 8: //select logic mode
      encoderMode = 9;
      logic1 = outParam[paramA][6];
      if (logic1 == 0) displayLED("L1-A");
      else if (logic1 == 1) displayLED("L1-B");
      else if (logic1 == 2) displayLED("L1-C");
      else if (logic1 == 3) displayLED("L1-D");
      else if (logic1 == 4) displayLED("L1-E");
      else if (logic1 == 5) displayLED("L1-F");
    break;
 
  case 9: //logic input 1
      encoderMode = 10;
      logic2 = outParam[paramA][7];
      if (logic2 == 0) displayLED("L2-A");
      else if (logic2 == 1) displayLED("L2-B");
      else if (logic2 == 2) displayLED("L2-C");
      else if (logic2 == 3) displayLED("L2-D");
      else if (logic2 == 4) displayLED("L2-E");
      else if (logic2 == 5) displayLED("L2-F");
    break;
   
  case 10: //logic input 2
      encoderMode = 11;
      patternOffset = outParam[paramA][1];
      tempString = String(patternOffset - 128) ;
      displayLED(tempString);
    break;
   
  case 11: // + offset (delay)
      encoderMode = 7;
      trigGate = outParam[paramA][2];
      if (trigGate) displayLED("GATE");
      else displayLED("TRIG");
    break;
 
  case 12: //Euclidean #beats
      encoderMode = 13; //go to #groups
      EuclidGroups = outParam[paramA][9];
      if (EuclidGroups > 99) tempString = "G";
      else if (EuclidGroups > 9) tempString = "G ";
      else tempString = "G  ";
      tempString += EuclidGroups;
      displayLED(tempString);
    break;
 
  case 13: //Euclidean #groups
      encoderMode = 6; //go to adjust offset
      patternOffset = outParam[paramA][1];
      tempString = String(patternOffset - 128);
      displayLED(tempString);
    break;
 
  case 14: //Save & Read parameters from EEPROM
      if (saveFunction == 1) { //go to Save settings
        encoderMode = 15;
        if (saveSlot > 9) tempString = "S ";
        else tempString = "S  ";
        tempString += saveSlot;
        displayLED(tempString);
      }
   
      else if (saveFunction == 2) {
        encoderMode = 16; //go to Read settings
       if (saveSlot > 9) tempString = "L ";
       else tempString = "L  ";
       tempString += saveSlot;
       displayLED(tempString);
      }
   
      else encoderMode = 1; //go back to rate
      //BPM = String(15000/rateMain);
      //displayLED(BPM);
    break;

  case 15: //Save settings
      saveSettings();
      encoderMode = 1; //go back to rate
      BPM = String(15000/rateMain);
      displayLED(BPM);
    break;
 
  case 16: //Load settings
      loadSettings();
      encoderMode = 1; //go back to rate
      BPM = String(15000/rateMain);
      displayLED(BPM);
    break;
 
  case 17: //Adjust swing
      encoderMode = 1; //go back to rate
      BPM = String(15000/rateMain);
      displayLED(BPM);
    break;
 
  default:
      encoderMode = 1;
  }
}

Here's the Important Stuff for the Triggerhappy module. I'll post more here when I get a chance. For the latest check the post on the Muffwiggler DIY forum .






Wednesday, April 9, 2008

LCD menu is working!

My latest struggle with the PID project has been getting the LCD menu system to work right. I was able to get the basic readout, showing temp, target temp, machine uptime, and heat power pretty easily. But I wanted to have a way to change the target, show the PID internal values, enter a standby mode, and adjust backlight level. I put some plain buttons in my project box and got them to work with the Arduino no problem, but getting the menus to work how I wanted has been trickier.

Not being a programmer, debugging has been a nightmare. I kept running into bizarre problems and having no idea where they were coming from. So I basically rewrote my LCD code. I also found a proper way to debounce my buttons and avoid double-presses. So now everything is working!

My next goal for this project is to attach a relay or sensor to the steam switch so I can have the PID control steaming temp, and the same for the brew switch, so I can tweak the control values during brewing and maybe put in a cute little little brewhead light so I can see what's going in the cup better. And also add a shot clock (just a readout, not actually turning off the brew process.) And I think after that, I may be done!

Saturday, March 22, 2008

Temperature problem solved ?

(This post refers to software from the Bare Bones Coffee Controller project by Tim Hirzel on the Arduino playground.)

So, I think I solved the problem with the temp sensing (see previous post). The thing that seems to have done it is to change the update interval to 2x per second from 5x:

#define PID_UPDATE_INTERVAL 500 // milliseconds

I re-read the PID without a PhD article, and it talks about sample rate. Since the boiler heater is a pretty slow-responding system, I figured I would try slowing down the rate that I grab a new temp for the PID process. I haven't changed how often the main loop checks the temp, so in effect, what I have done is increased the sample size that getFreshTemp() uses in averaging temps. But at this point, I don't really care, as long as it works. I'm also doing some extra smoothing with a simple algorithm I grabbed from the Arduino playground:

#define filterVal .2 //between 0 and 1 for smoothing function (small=more smooth)
float accum; //storage for smoothing function

float getFreshTemp() {
latestReading = (tcSum * multiplier / readCount) + 32.0;
accum = (latestReading * filterVal) + (accum * (1 - filterVal)); //smoothing function
latestReading = accum;
readCount = 0;
tcSum = 0.0;
return latestReading;


My performance is great now! The problem with the spikey temps was that they made the D factor essentially useless. Now, I've been able to tune the PID so that it stays within .5 deg +/-.

Friday, March 21, 2008

Temp sensing problems

Here's a little problem I'm having with the PID. My temp spikes every time the Arduino turns on the digital pin connected to the SSR (to turn on the heater). Here's what the graph looks like:

The measured temp goes up a couple of degrees when the Arduino is driving a digital pin attached to the SSR. Obviously, the actual temp isn't doing this. You can see that the duration of the error corresponds to the duty cycle - the upper part of the graph is wider when the SSR is on for a longer time, and thins out as the temp gets closer to the setpoint. I had thought this might be caused by the voltage dropping because of the heater sucking so much power (800 watts?) but it does the same thing when Silvia is powered off and unplugged. If I disconnect the (-) connection to the SSR, the error increases, but when I remove the (+) side, it goes away. Another strange error is that when my MacBook is plugged in to the AC adapter, the overall temp measurement goes up about five degrees. (I normally don't drag my adapter into the kitchen.) I've tried powering the Arduino from my adapter inside Silvia, an external adapter, an external adapter on a different circuit, and from the Mac's usb, all with the same result.

Because of the way the temp goes up when the Mac is plugged in, I wonder if this only happens when attached to the Mac. But I can't check this since I wouldn't be able to see the temp without the Mac. I may try to hook up my LCD display so I can monitor the temp without connecting to the Mac.

Friday, March 7, 2008

Some background on the PID project

When I first got my Silvia espresso machine about three years ago, I was really interested in all the mods people were doing. I did the pressure modification because it was relatively easy, cheap, and it seemed to me that it would improve my espresso. I really wanted to install a PID too, but I stopped short because using a commercial PID controller seemed overly expensive to me. So I learned to "reverse time surf" and made do with that for a long time. The problem with that is that it takes a long time between shots, and without a temperature readout, its still guesswork.

PID stands for proportional, integral, derivative, which are the elements of a formula for controlling a closed process (one input, one output) within a very tight range. The cruise control on your car is a PID controller. Basically the formula looks at the current error (distance from desired temperature) and adjusts the power to the heating element based on the sum of: the current error (P), the sum of errors over time (I) and the current rate of change of the error (D). For more details check out the wikipedia page on PID controllers.

The reason for using a PID to control the boiler on an espresso machine is that "perfect" espresso requires (among other things) a very specific temperature (depending on the coffee used) and the temperature must be kept as close as possible to that temp throughout the brewing process. The Silvia is a good candidate for a PID, since the temperature is already pretty stable for a relatively inexpensive home machine. (To get a more stable temp, you'd have to spend at least twice as much as Silvia.) In actuality, the PID has nothing to do with keeping the temp stable during the pour, it only helps the machine recover quickly without overshoot. Temp stability is a result of the thermal mass of the brass in the machine, which the Silvia has quite a lot of. The PID also makes it possible to precisely control the starting temp, which to me is the biggest advantage. Here's a blog that has some temperature plots so you can see the difference between PID and non-PID temperature ranges.

So, why am I doing this now? Hmmmm.... Well it started with the Make magazine video podcasts. I got addicted to them, and several of their projects used this thing called an "Arduino". I was like, "what the heck it that." I looked it up and found out it was an open source microcontroller. I had built a robot with a Basic Stamp several years ago, so my interest was piqued. Then, I ran across Tim Hirzel's and Nash Lincoln's blogs where they talked about using the Arduino for PID. Now all my lights were lit up.

As I've gotten into this, I think that if someone just wanted a simple PID, it would be easier to go the traditional route, using a dedicated PID controller. (Murph's PID page is the canonical reference.) There are several people selling "PID kits" that make it really simple. But the cool thing about using a full-blown microprocessor like the Arduino is that I can add as many features as I want. I plan to add a brewhead light that will light up during shots, using the PID to control steaming, and a custom LCD display. Tim and Nash have are taking it way further than that, doing things like using a nintendo controllers for remote control and more!

Thursday, March 6, 2008

About the DIY revolution

I saw this post by Clive Thompson yesterday on Wired. It really sums up a lot of what I love about tweaking and tinkering with stuff. I get a lot of intellectual stimulation from learning about stuff and how to fix it. I also like the idea of reusing things that would otherwise go in the landfill and not buying new unless I have to.

Wednesday, March 5, 2008

...and so it begins.

This is the beginning of my blog to talk about my "personal" projects. The one I'm on right now is putting a PID controller on my Silvia espresso machine, using on the Arduino microcontroller. I'm basing it on work done by Tim Hirzel at the Arduino playground.

The basic idea is to replace the stock brew thermostat with a solid state relay (SSR) to control the boiler heater. The SSR is controlled by a microcontroller running software that takes a temperature input from the boiler and through the PID formula, keeps the temperature in a very tight range, resulting in superior espresso. I plan to put details about the project in future posts.