/*
 * Sketch for counting impulses in a defined interval
 * e.g. for power meters with an s0 interface that can be 
 * connected to an input of an arduino board 
 *
 * the sketch uses pin change interrupts which can be anabled 
 * for any of the inputs on e.g. an arduino uno or a jeenode
 *
 * the pin change Interrupt handling used here 
 * is based on the arduino playground example on PCINT:
 * http://playground.arduino.cc/Main/PcInt
 *
 * Refer to avr-gcc header files, arduino source and atmega datasheet.
 */

/* Pin to interrupt map:
 * D0-D7 = PCINT 16-23 = PCIR2 = PD = PCIE2 = pcmsk2
 * D8-D13 = PCINT 0-5 = PCIR0 = PB = PCIE0 = pcmsk0
 * A0-A5 (D14-D19) = PCINT 8-13 = PCIR1 = PC = PCIE1 = pcmsk1
 */

#include "pins_arduino.h"

char* version = "ArduCounter V1.0";
char* error = "error ";

/* arduino pins that are typically ok to use 
 * (some are left out because they are used 
 * as reset, serial, led or other things on most boards) */
byte allowedPins[20] = 
  { 0,  0,  0,  3,  4, 5, 6, 7,
    0,  9, 10, 11, 12, 0,
   14, 15, 16, 17,  0, 0};


/* Pin change mask for each chip port */
volatile uint8_t *port_to_pcmask[] = {
  &PCMSK0,
  &PCMSK1,
  &PCMSK2
};

/* last PIN States to detect individual pin changes in ISR */
volatile static uint8_t PCintLast[3];


unsigned long intervalMin =  10000; // default 10 sec
unsigned long intervalMax = 120000; // default 2 min

unsigned long timeNextReport;
unsigned long now;

boolean doReport = false;

/* index to the following arrays is the internal PCINT pin number, not the arduino 
 * pin number because the PCINT pin number corresponds to the physical ports
 * and this saves time for mapping to the arduino numbers
 */

/* pin change mode (RISING etc.) as parameter for ISR */
byte PCintMode[24];

/* pin number for PCINT number if active - otherwise -1 */
char PCintActivePin[24];

/* did we get first interrupt yet? */
volatile boolean initialized[24];
 
/* individual counter for each real pin */
volatile unsigned long counter[24];

/* count at last report to get difference */
unsigned long lastCount[24];

/* millis at first interrupt for current interval
 * (is also last interrupt of old interval) */
volatile unsigned long startTime[24];

/* millis at last interrupt */
volatile unsigned long lastTime[24];

/* millis at last report 
 * to find out when maxInterval is over
 * and report has to be done even if
 * no impulses were counted */
unsigned long lastReport[24];


/* max for SplitLine */
#define MAXLINEPARTS 5

String inputString = "";         // a string to hold incoming data
boolean newCommand = false;      // whether the command string is complete

String linePart[MAXLINEPARTS];
int lineParts = 0;


/* Add a pin to be handled */
int AddPin(uint8_t aPin, int mode) {
  uint8_t pcintPin;                         // PCINT pin number for the pin to be added (this is used as index for most arrays)
  volatile uint8_t *pcmask;                 // pointer to PCMSK0 or 1 or 2 depending on the port corresponding to the pin
  unsigned long now = millis();
  
  uint8_t bit = digitalPinToBitMask(aPin);  // bit in PCMSK to enable pin change interrupt for this pin (arduino pin number!)
  uint8_t port = digitalPinToPort(aPin);    // port that this pin belongs to for enabling interrupts for the whole port (arduino pin number!)

  if (port == NOT_A_PORT) {
    return 1;
  } else {                              // map port to bit in PCIR register
    port -= 2;
    pcmask = port_to_pcmask[port];      // point to PCMSK0 or 1 or 2 depending on the port corresponding to the pin
  }
  if (port == 1) {                      // now calculate the PCINT pin number that corresponds to the arduino pin number
     pcintPin = aPin - 6;                // port 1: PC0-PC5 (A0-A5 or D14-D19) is PCINT 8-13 (PC6 is reset)
  } else {                              // arduino numbering continues at D14 since PB6/PB7 are used for other things 
     pcintPin = port * 8 + (aPin % 8);  // port 0: PB0-PB5 (D8-D13) is PCINT 0-5 (PB6/PB7 is crystal)
  }                                     // port 2: PD0-PD7 (D0-D7) is PCINT 16-23
  
  PCintMode[pcintPin] = mode;           // save mode for ISR which uses the pcintPin as index because this is easy to get in ISR
  PCintActivePin[pcintPin] = aPin;      // save real arduino pin number and flag this pin as active for reporting

  initialized[pcintPin] = false;        // initialize arrays for this pin
  counter[pcintPin]     = 0;            
  lastCount[pcintPin]   = 0;
  startTime[pcintPin]   = now;     
  lastTime[pcintPin]    = now;
  lastReport[pcintPin]  = now;
  
  *pcmask |= bit;          // set the pin change interrupt mask through a pointer to PCMSK0 or 1 or 2 depending on the port corresponding to the pin
  PCICR |= 0x01 << port;   // enable the interrupt
  return 0;
}


/* Remove a pin to be handled */
int RemovePin(uint8_t aPin) {
  uint8_t pcintPin;
  volatile uint8_t *pcmask;
  uint8_t bit = digitalPinToBitMask(aPin);
  uint8_t port = digitalPinToPort(aPin);

  if (port == NOT_A_PORT) {
    return 1;
  } else {
    port -= 2;
    pcmask = port_to_pcmask[port];
  }
  if (port == 1) {                       // see comments at AddPin above
     pcintPin = port * 8 + (aPin - 14);
  } else {
     pcintPin = port * 8 + (aPin % 8);
  }
  PCintActivePin[pcintPin] = -1;

  *pcmask &= ~bit;      // disable the mask.
  if (*pcmask == 0) {   // if that's the last one, disable the interrupt.
    PCICR &= ~(0x01 << port);
  }
  return 0;
}


// common interrupt handler. "port" is the PCINT port number (0-2)
static void PCint(uint8_t port) {
  uint8_t bit;
  uint8_t curr;
  uint8_t mask;
  uint8_t pcintPin;

  // get the pin states for the indicated port.
  curr = *portInputRegister(port+2);         // current pin states at port
  mask = curr ^ PCintLast[port];             // xor gets bits that are different
  PCintLast[port] = curr;                    // store new pin state for next interrupt

  if ((mask &= *port_to_pcmask[port]) == 0) {  // mask is pins that have changed. screen out non pcint pins.
    return; /* no handled pin changed */
  }

  for (uint8_t i=0; i < 8; i++) {
    bit = 0x01 << i;                         // loop over each pin that changed
    if (bit & mask) {                        // is pin change interrupt enabled for this pin?
      pcintPin = port * 8 + i;               // pcint pin numbers directly follow the bit numbers, only arduino pin numbers are special

      // count if mode is CHANGE, or if mode is RISING and
      // the bit is currently high, or if mode is FALLING and bit is low.
      if ((PCintMode[pcintPin] == CHANGE
          || ((PCintMode[pcintPin] == RISING) && (curr & bit))
          || ((PCintMode[pcintPin] == FALLING) && !(curr & bit)))) {
        lastTime[pcintPin] = millis();       // remember time of this impulse in case it will be the last in the interval
        if (initialized[pcintPin]) {
          counter[pcintPin]++;               // count
        } else {
          startTime[pcintPin] = lastTime[pcintPin]; // if this is the first impulse on this pin, remember time as first impulse in interval
          initialized[pcintPin] = true;             // and start counting the next impulse
        }
      }
    }
  }
}


SIGNAL(PCINT0_vect) {
  PCint(0);
}
SIGNAL(PCINT1_vect) {
  PCint(1);
}
SIGNAL(PCINT2_vect) {
  PCint(2);
}


/* split a line read from serial into individual words 
 * and store them in the array lineParts[] */
void SplitLine () {
  int index = 0;
  int sepPos = 0;
  lineParts = 0;
  while (sepPos > -1 && index < inputString.length()) {      // as long as a blank was found an not at end of line ...
    while (inputString.charAt(index) == ' '                  // if next char is another blank
            && lineParts < MAXLINEPARTS
            && index < inputString.length()) index++;        // skip more blanks
    if (index < inputString.length()) { 
      sepPos = inputString.indexOf(' ', index+1);            // find next blank after the word?
      if (sepPos == -1)                             
        linePart[lineParts] = inputString.substring(index);  // no more blanks -> take rest of string
      else 
        linePart[lineParts] = inputString.substring(index, sepPos); // more blanks -> take word nefore next blank and go on
      index = sepPos + 1;                                    // continue looking after last blank
      lineParts ++;
    }
  }
  for (int i=lineParts; i<MAXLINEPARTS; i++)                 // clear the rest of the array
    linePart[i] = "";
}



void report() {
  int aPin;
  unsigned long newCount, countDiff;
  unsigned long timeDiff, now;
  now = millis();
  for (int pcintPin=0; pcintPin<24; pcintPin++) {             // go through all observed pins as PCINT pin number
    aPin = PCintActivePin[pcintPin];                          // take saved arduino pin number
    if (aPin >= 0) {                                          // -1 means pin is not active for reporting
      newCount  = counter[pcintPin];                          // get current counter
      countDiff = newCount - lastCount[pcintPin];             // how many impulses since last report?
      if (countDiff == 0 && 
         (now - lastReport[pcintPin] < intervalMax))          // if nothing to report, take next pin
        continue;                                 
      if (countDiff > 0) {                                    // if there was an impulse, report
        timeDiff  = lastTime[pcintPin] - startTime[pcintPin]; // time between first and last impulse during interval
        lastCount[pcintPin] = newCount;                       // remember current count for next interval
        startTime[pcintPin] = lastTime[pcintPin];             // time of last impulse in this interval becomes also time of first impulse in next interval
      } else {                                    
        timeDiff  = now - startTime[pcintPin];                // there was no impulse, but maxInterval is over: show from last impulse to now
        startTime[pcintPin] = now;                            // start a new interval for next report - last one will be reported as 0 imulses ... 
        lastTime[pcintPin] = now;                             // also time of first impulse in next interval
      }
      Serial.println((String) "R" + aPin +                    // report on serial out
                              " C" + newCount + 
                              " D" + countDiff + 
                              " T" + timeDiff);             
      lastReport[pcintPin] = now;                             // remember when we reported 
    }
  }
}


/* give status report in between if requested over serial input */
void showCmd() {
  unsigned long newCount;
  unsigned long countDiff;
  unsigned long timeDiff;
  char* pName;

  Serial.println((String) version); 
  Serial.println((String) "Min " + intervalMin);
  Serial.println((String) "Max " + intervalMax);
  
  for (int i=0; i<24; i++) {
    int aPin = PCintActivePin[i];
    if (aPin != -1) {
      timeDiff = lastTime[i] - startTime[i];
      newCount = counter[i];
      countDiff = newCount - lastCount[i];
      if (!timeDiff)
        timeDiff = millis() - startTime[i];
      Serial.println((String) "PCInt " + i + " aPin " + aPin
                            + " Cnt " + newCount + " (+" + countDiff 
                            + " ) in " + timeDiff + " Millis");
    }
  }
  Serial.println((String) "Next in " + (timeNextReport - millis()));
}


void addCmd() {
  String pinArg = linePart[1];              // given arduino pin number or name as string
  String modeArg = linePart[2];             // mode falling, rising or change
  String pullArg = linePart[3];             // optional pullup
  int aPin = -1;                             
  int mode = RISING; // default

  if (pinArg.charAt(0) == 'D' || pinArg.charAt(0) == 'd') 
    aPin = pinArg.substring(1).toInt();      // arduino pin name starting with a "d"?
  if (aPin == -1) {
    aPin = pinArg.toInt();                   // interpret string as pin number
    if (aPin >= 20 || aPin < 1) {
      Serial.print(error); Serial.println(aPin);
      return;
    } 
  }  
  if (allowedPins[aPin] == 0) {
    Serial.print(error); Serial.println(aPin);
    return;
  };
  if (lineParts > 2) {
    if (modeArg.equalsIgnoreCase("f"))
      mode = FALLING;
    else if (modeArg.equalsIgnoreCase("c"))
      mode = CHANGE;
    else if (modeArg.equalsIgnoreCase("r"))
      mode = RISING;
    else {
      Serial.print(error); Serial.println(modeArg);
      return;
    }
  }
  pinMode (aPin, INPUT);
  if (lineParts > 3) {
    if (pullArg.equalsIgnoreCase("p"))
      digitalWrite (aPin, HIGH);             // enable pullup resistor
    else {
      Serial.print(error); Serial.println(pullArg);
      return;
    }
  }
  
  if (AddPin(aPin, mode) == 0) {             // call AddPin with arduino pin number
    Serial.print("added "); Serial.println(aPin);
  } else {
    Serial.print(error); Serial.println(pinArg);
  }
}


void removeCmd() {
  String pinArg = linePart[1];              // given arduino pin number or name as string in first part
  int aPin = -1;
  if (pinArg.charAt(0) == 'D' || pinArg.charAt(0) == 'd') 
    aPin = pinArg.substring(1).toInt();      // arduino pin name starting with a "d"?
  if (aPin == -1) {
    aPin = pinArg.toInt();                  // interpret string as pin number
    if (aPin >= 20 || aPin < 1) {
      Serial.print(error); Serial.println(aPin);
      return;
    } 
  }  
  if (allowedPins[aPin] == 0) {
    Serial.print(error); Serial.println(aPin);
    return;
  };
  if (RemovePin(aPin) == 0) {               // call RemovePin with arduino pin number
    Serial.println((String)"removed " + aPin);
  } else {
    Serial.print(error); Serial.println(pinArg);
  }
}


void intervalCmd() {
  String timeArgMin = linePart[1];
  String timeArgMax = linePart[2];
  int timeMin = timeArgMin.toInt();
  if (timeMin < 1 || timeMin > 3600) {
    Serial.print(error); Serial.println(timeArgMin);
    return;
  }
  int timeMax = timeArgMax.toInt();
  if (timeMax < 1 || timeMax > 3600 || timeMax < timeMin) {
    Serial.print(error); Serial.println(timeArgMax);
    return;
  }
  intervalMin = (long)timeMin * 1000;
  intervalMax = (long)timeMax * 1000;
  if (millis() + intervalMin < timeNextReport)
    timeNextReport = millis() + intervalMin;
}


void doCommand() {
  SplitLine();
  if (linePart[0].equals("show")) {
    showCmd();
  } else if (linePart[0].equals("add")) {
    addCmd();
  } else if (linePart[0].equals("rem")) {
    removeCmd();
  } else if (linePart[0].equals("int")) {
    intervalCmd();
  } else {
    Serial.print(error); Serial.println(linePart[0]);
  }
}


void setup() {
  unsigned long now = millis();
  for (int pcintPin=0; pcintPin < 24; pcintPin++) {
    PCintActivePin[pcintPin] = -1;          // set all pins to inactive (-1)
  } 
  timeNextReport = millis() + intervalMin;  // time for first output
  Serial.begin(9600);                       // initialize serial
  inputString.reserve(200);                 // reserve 200 bytes for the inputString
  interrupts();
  Serial.println((String) version + " Setup done."); 
}



void loop() {
  now = millis();
  doReport = false;                              // check if report nedds to be called
  if((long)(now - timeNextReport) >= 0)
    doReport = true;                             // intervalMin is over 
  else 
    for (byte pcintPin=0; pcintPin<24; pcintPin++)  
      if (PCintActivePin[pcintPin] >= 0)
        if((long)(now - (lastReport[pcintPin] + intervalMax)) >= 0)
          doReport = true;                       // active pin has not been reported for langer than maxInterval
  if (doReport) {    
    report();
    timeNextReport = now + intervalMin;          // do it again after interval millis
  }
  if (newCommand) {
    doCommand();
    inputString = "";
    newCommand = false;
  }
}


/*
 SerialEvent occurs whenever a new data comes in the
 hardware serial RX.  This routine is run between each
 time loop() runs, so using delay inside loop can delay
 response.  Multiple bytes of data may be available.
 */
void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read(); 
    if (inChar == '\n' or inChar == '\r') {
      if (inputString.length() > 0)
        newCommand = true;
    }  else {
      inputString += inChar;
    }
  }
}


