// ATmega48 lightshow controller 
// 2009 Dany Qumsiyeh (qhex.org)
//
// Provides fading control over serial for 11 solid-state relays.
// Requires a sync input that indicates when AC voltage is
// positive.
//
// PB1-5,PC0-5 = output to SSR, inverted
// PD0 = rxd, serial in
// PD1 = txd, serial out
// PD2 = AC sync input
//
// serial protocol:
// '$' starts manual/single mode, with all lights selected
//   letters 'a' through 'k' select a set of lights
//   numbers '0' to '9' set a light level for all selected lights
// '#' starts a batch command
//   subsequent ascii characters set the light levels in order
//   valid values are ascii 37 (full on) through 255 (off)
//
// for example, the following turns all lights off, then the first
// three lights on full: $0abc9
//
// default on power-up is manual mode, with all lights off

#include <avr/io.h>
#include <avr/interrupt.h>
#include <inttypes.h>
#include <avr/pgmspace.h>

#define F_CPU 8000000
#define BAUD 9600

#define NUM_LIGHTS 11

// thresholds for each light
volatile uint8_t state[NUM_LIGHTS];
// threshold lookup table for values 0-9
volatile uint8_t fade_values[10];

enum { MODE_SINGLE, MODE_BATCH };
volatile uint8_t mode = MODE_SINGLE;

// single mode
enum { SELECT_ADD, SELECT_REPLACE };
volatile uint8_t select_mode = SELECT_REPLACE;
// bit mask for selected lights
volatile uint16_t selection = 0xFFFF;

// batch mode
volatile uint8_t batch_i = 0;

int main(void) {
  uint8_t i;

  // set up ports
  DDRB = _BV(1)|_BV(2)|_BV(3)|_BV(4)|_BV(5);
  DDRC = _BV(0)|_BV(1)|_BV(2)|_BV(3)|_BV(4)|_BV(5);
  DDRD = _BV(1);  // serial tx
  PORTB = 0xFF;  // outputs are inverted
  PORTC = 0xFF;

  // set up serial
  uint8_t ubrr = F_CPU/16/BAUD-1;  // baud rate
  UBRR0H = (uint8_t)(ubrr >> 8);
  UBRR0L = (uint8_t) ubrr;
  // enable rx & tx
  UCSR0B = _BV(RXCIE0) | _BV(RXEN0) | _BV(TXEN0);

  // set up sync interrupt
  EICRA = _BV(ISC00); // falling edge of INT0
  EIMSK = _BV(INT0);

  // set up timer
  TCCR0B = _BV(CS02);  // clk/256
  TIMSK0 = _BV(OCIE0A);  // enable output compare interrupt
  OCR0A = 255;

  // initialize variables
  for (i = 0; i < sizeof(state); i++) {
    state[i] = 255;
  }
  fade_values[0] = 255;
  fade_values[1] = 216;
  fade_values[2] = 204;
  fade_values[3] = 192;
  fade_values[4] = 180;
  fade_values[5] = 168;
  fade_values[6] = 156;
  fade_values[7] = 140;
  fade_values[8] = 116;
  fade_values[9] = 0;

  // enable interrupts
  sei();

  // set outputs based on the timer value, in a tight loop
  while (1) {
    uint16_t output = 0;
    uint8_t t = TCNT0;
    for (i = 0; i < sizeof(state); i++) {
      if (t > state[i])
	output |= _BV(i);
    }
    PORTB = ~(uint8_t)(output << 1);
    PORTC = ~(uint8_t)(output >> 5);
  }
}

ISR(USART_RX_vect) {
  uint8_t i;
  uint8_t c = UDR0;
  if (c == '$') {  // start single mode
    mode = MODE_SINGLE;
    select_mode = SELECT_REPLACE;
    selection = 0xFFFF;
  } else if (c == '#') {  // start batch mode
    mode = MODE_BATCH;
    batch_i = 0;
  } else if (mode == MODE_SINGLE) {  // single mode
    if (c >= 'a' && c <= 'a' + 16) {
      if (select_mode == SELECT_REPLACE) selection = 0;
      selection |= _BV(c - 'a');
      // once a selection is started, allow additions
      select_mode = SELECT_ADD;
    } else if (c >= '0' && c <= '9') {
      for (i = 0; i < sizeof(state); i++)
	if (selection & _BV(i))
	  state[i] = fade_values[c - '0'];
      // once a command is given, the next
      // selection will start from scratch
      select_mode = SELECT_REPLACE;
    }
  } else if (batch_i < sizeof(state)) {  // batch mode
    if (c >= 37) {
      state[batch_i] = c;
    }
    batch_i++;
  }
}

// set the timer threshold based on the sync signal
ISR(INT0_vect) {
  if (PIND & _BV(2)) {  // rising edge
    OCR0A = TCNT0 + 60;
  } else {  // falling edge
    OCR0A = TCNT0 + 42;
  }
}

// reset timer on compare match
ISR(TIMER0_COMPA_vect) {
  TCNT0 = 0;
  OCR0A = 255;
}
