/*
 * Whiskey Pirates Badge "PEGLEG CPU"
 * Sub MCU Firmware for ATtiny88
 *
 * Created: 5/21/2014 9:24:48 PM
 *  Author: true
 * ts:4
 *
 ********
 * PEGLEG CPU is responsible for:
 * - aux gpio (todo: list specifically which GPIOs on the badge)
 * - eye RGBLED matrix
 * - temperature sensor
 * - light sensor (using eye RGBLEDs; in practice uses the left eye red LED)
 * - user config eeprom
 *
 ********
 * Use the following fuses for this project. Needs to run at 8MHz for 400khz I2C operation.
 * 8MHz, self-program enabled (bootloader support), preserve EEPROM, BOD disabled, ISP enabled
 * -U lfuse:w:0xEE:m -U hfuse:w:0xD7:m -U efuse:w:0x00:m
 *       (efuse will likely show as 0x06 after setting; this is fine, don't worry about it)
 *
 * Resources used: Mainline ISR 5% (3/63 timer counts simulated, as of some old rev)
 *                 Flash 26.8%, SRAM 10.4% (as of the above rev)
 *
 **********
 * Register Variables:
 *		main.c:comm_cmd:r15
 *		adc.h:adc_read_step:r14
 *		led.c:rgbled_idx:r13
 */


#include "config.h"


/* system */
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>

#include <util/delay.h>


#include "i2c.h"

#include "timer.h"
#include "adc.h"
#include "led.h"



/* modes (commands) */
#define	MODE_NONE					0b0000

#define	MODE_EXT_CMD				0b0001		// bit 4 unset = extra parameters needed, up to 6 commands
#define	MODE_LED_SET_LEVEL			0b0010 		// (0x00 reserved, 0x1? extra, leaving 0x2? through 0x7?)
#define MODE_TEMPSENSOR_CAL			0b0011
#define	MODE_EEPROM_READ			0b0100
#define	MODE_EEPROM_WRITE			0b0101

#define	MODE_TEMPSENSOR_READ		0b1000		// bit 4 set = immediate, no extra parameters, process now
#define	MODE_LIGHTSENSOR_READ		0b1001
#define MODE_SLEEP					0b1111		

#define	MODE_AUX_PIN_GET			0x10		// extended (send 0x10, cmd, data...)
#define	MODE_AUX_PIN_SET			0x11		// ext commands MUST be 0x10 or higher! 0x00-0x0f are RESERVED!
#define MODE_LIGHTSENSOR_SENS 		0x19

/* aux pin mode */
#define	AUX_PIN_COUNT				10
#define	AUX_PIN_OUTPUT				0x01		// sets to output if set, input if cleared
#define	AUX_PIN_PORTSET				0x02		// output: sets high/low; input: sets pullup on/off

/* adc modes */
#define	ADC_MODE_TEMPSENSOR			8
#define	ADC_MODE_LIGHTSENSOR		1


/* prototypes */
static void system_init();
static void system_io_init();
static void tempsensor_cal();


/* globals */
static uint8_t tim0_milli;
static uint8_t tim0_centi;
static uint8_t tim0_profiler;

#define COMM_DATA_SIZE 				4 		// must be power of 2; default value is 4
register uint8_t comm_cmd asm("r15");		// currently processed SPI command
uint8_t comm_data_idx;						// command data write index
uint8_t comm_data[COMM_DATA_SIZE];			// command data register
uint8_t comm_timeout;						// command invalidate timeout


/* core */
static uint8_t pirate_sleep_mode = SLEEP_MODE_IDLE;

/* tempsensor */
static int8_t temperature;
static int16_t temp_offset;

/* eeprom */
#define	EEPROM_ADDR_TEMPCAL			62		// 2 bytes

/* adc */
volatile uint8_t adc_busy;
volatile uint16_t adc_result[ADC_MAX_FEEDBACK + 1];

/* led */
uint8_t rgbled_pwm_lf[4];					// pwm value for TIMER1 OCA LED outputs
uint8_t rgbled_pwm_rt[4];					// pwm value for TIMER1 OCB LED outputs

uint8_t rgbled_light_level[4];



/* it begins */
int main(void)
{
	// configure device and IO
	system_init();
	system_io_init();	
	
	// configure RGBLEDs
	rgbled_io_init();
	
	// configure i2c communications
	i2c_slave_init(I2C_SLAVE_ADDRESS, I2C_ENABLE_ADDR_ZERO);

	// configure system timer
	timer0_init();
	// configure led pwm timer
	timer1_init();
	
	// initialize adc
	adc_init();
	
	// try calibrating temperature sensor if needed
	tempsensor_cal();
	
	// and now we wait.
    while (1) {
		// set sleep mode, then set for idle sleep (CPU off, all peripherals on)
		set_sleep_mode(pirate_sleep_mode);
		pirate_sleep_mode = SLEEP_MODE_IDLE;
		
		// re-enable interrupts and nap.
		sei();
		sleep_mode();
    }
}


/* func */
static void system_init()
{
	// make SURE we are running at 8MHz
	// do this by first enabling clock divider setup mode,
	CLKPR = _BV(CLKPCE);
	// and disabling any divider
	CLKPR = 0;
}

static void system_io_init()
{
	// enable break-before-make outputs on all IO
	PORTCR |= (_BV(BBMA) | _BV(BBMB) | _BV(BBMC) | _BV(BBMD));
	
	// set ALL pins as inputs except PB0
	DDRA = 0x00;
	DDRB = 0x01;
	DDRC = 0x00;
	DDRD = 0x00;
	
	// and enable pullups (this reduces power consumption)
	// in the case of PB0, set it LOW
	PORTA = 0xff;
	PORTB = 0xfe;
	PORTC = 0xff;
	PORTD = 0xff;
}

/* temperature */
uint8_t tempsensor_process()
{
	int8_t temp_gain;
	int8_t temp_adj;
	
	temp_adj = adc_result[ADC_CHAN_TEMP] - temp_offset;		// our offset adjusted temperature
	temp_gain = temp_adj >> 3;								// our gain adjust amount
	while (temp_gain > 0) {temp_gain--; temp_adj--;};		// correct positive gain
	while (temp_gain < 0) {temp_gain++; temp_adj++;};		// correct negative gain
	
	// save temperature
	return temp_adj;
}

void tempsensor_read()
{
	if (adc_read_step == 0) {
		// select temperature channel
		adc_channel(ADC_CHAN_TEMP, ADC_REF_BANDGAP);
		
	} else if (adc_read_step == 3) {
		// we've waited 1.5ms so the bandgap voltage should have been set.
		// start conversion after bandgap ref change timeout per datasheet
		adc_start(4, 1);
		
	} else if (adc_read_step > 3) {
		adc_read_step--;
		
		if (!adc_busy) {
			// revert reference to AVCC...per the datasheet only changes to
			// bandgap should take ~1ms but changes back to AVCC seem to
			// take a while to be accurate as well
			adc_channel(ADC_CHAN_GND, ADC_REF_AVCC);
			adc_read_mode = 0;
			temperature = tempsensor_process();
		}
	}
}

void tempsensor_cal()
{
	// NOTE: CALIBRATION IS SINGLE-POINT TO 0 DEGREES CENTIGRADE! ICE YOUR PEGLEG, MOTHERFUCKER!
	// todo: figure out wtf I meant by this note in 2014 (this is me in 2023)
	// looking at the code, it looks like a pin needs manually changed to calibrate.
	// todo: add a zeroing offset calibration command to the command protocol?
	
	// is pin PA2 low? (pin 3 on attiny88 - is pulled up - short to pin 5 / ground somewhere)
	if ((PINA & _BV(PINA2)) == 0) {
		// turn on the ADC, wait for it to warm up
		adc_init();
		_delay_ms(100);				// todo: is this right? this seems slow
		
		// is the pin still low? not a glitch?
		if ((PINA & _BV(PINA2)) == 0) {
			// enable interrupts
			sei();
			
			// start reading the temp sensor; stall until adc is done
			adc_read_mode = ADC_MODE_TEMPSENSOR;
			tempsensor_read();
			while (adc_busy);
			
			// here's our correction factor!
			temp_offset = adc_result[ADC_CHAN_TEMP];
			
			// write it to eeprom
			eeprom_write_word((uint16_t *)EEPROM_ADDR_TEMPCAL, temp_offset);
			
			// and that's it - we're done.
			adc_read_mode = 0;
			return;
		}
	}
	
	// not saving a value - read the value from eeprom
	temp_offset = eeprom_read_word((uint16_t *)EEPROM_ADDR_TEMPCAL);
	
	// does it seem invalid?
	if (temp_offset > 560) {
		// yeah, this isn't right. load rough value for 0degC from datasheet.
		// but you really should calibrate your chip, you lazy fuck.
		temp_offset = 273;
	}
}

/* comms */
static inline uint8_t pegleg_data_tx()
{
	uint8_t ret;
	
	switch (comm_cmd) {
		case MODE_EEPROM_READ: {
			if (comm_data[0] < 64) {
				i2c_slave_tx(eeprom_read_byte((uint8_t *)(uint16_t)comm_data[0]), 1);
				ret = 1;
			} else {
				// invalid address...
				i2c_disable_slave();
				ret = 255;
			}
			comm_cmd = 0;
			comm_timeout = 0;
			return ret;
		}
		
		case MODE_TEMPSENSOR_READ: {
			i2c_slave_tx(temperature, 1);
			comm_cmd = 0;
			comm_timeout = 0;
			return 1;
		}
		case MODE_LIGHTSENSOR_READ: {
			i2c_slave_tx(adc_result[comm_data[0] & 0x03] >> 2, 1);
			comm_cmd = 0;
			comm_timeout = 0;
			return 1;	
		}
		
		default: {
			// nothing active; invalid :(
			i2c_disable_slave();
			return 255;
		}
	}
	
	return 0;
}

static uint8_t pegleg_cmd()
{
	switch (comm_cmd) {
		// no command: we just ACK this as we shouldn't ever get here
		case MODE_NONE: {
			comm_cmd = 0;
			comm_timeout = 0;
			break;
		}
		
		// standard commands
		case MODE_EXT_CMD: {		// used for more commands
			if (comm_data_idx >= 1) {
				comm_cmd = comm_data[1];
				// set data index to last (will be reset on next data sent)
				// and also reset timeout since this is effectively a new command
				comm_data_idx = COMM_DATA_SIZE - 1;
				comm_timeout = 0;
			}
			break;
		}

		case MODE_LED_SET_LEVEL: {		// sets PWM rate for the 8 PWM LEDs
			if (comm_data[0] < 4) {
				rgbled_pwm_lf[comm_data[0]] = comm_data[1];
			} else if (comm_data[0] < 8) {
				rgbled_pwm_rt[comm_data[0] - 4] = comm_data[1];
			}
			comm_cmd = 0;
			comm_timeout = 0;
			break;
		}
		
		case MODE_TEMPSENSOR_CAL: {
			// to use this function, make sure the temperature has been previously read.
			// calculates the appropriate offset based on the reported temperature.
			if (comm_data_idx >= 1) {
 				// update the offset
				if (adc_result[ADC_CHAN_TEMP]) {
					temp_offset = adc_result[ADC_CHAN_TEMP] - (int8_t)comm_data[1];
 					// write it to eeprom
 					eeprom_write_word((uint16_t *)EEPROM_ADDR_TEMPCAL, temp_offset);
 					// and that's it - we're done.
 					comm_cmd = 0;
					comm_timeout = 0;
				} else {
					// no valid read performed; I fucking told you to do this!
					comm_cmd = 0;
					comm_timeout = 0;
					return 1;
				}
			}
			break;
		}
		
		case MODE_EEPROM_READ: {
			// our data packet is already set; ready for read.
			// but if the host keeps writing...
			if (comm_data_idx > 1) {
				comm_cmd = 0;
				comm_timeout = 0;
			}
			break;
		}
		case MODE_EEPROM_WRITE: {
			// TODO: implement
			comm_cmd = 0;
			comm_timeout = 0;
			break;
		}

		// immediate commands
		case MODE_TEMPSENSOR_READ: {
			// attempt another read
			if (!adc_read_mode) {
				adc_read_mode = 0;
				adc_read_step = 0;
				adc_read_mode = ADC_MODE_TEMPSENSOR;
			}
			
			// now we are ready to read.
			// data will probably be prior data unless the host waits. (need to verify)
			comm_timeout = 0;
			break;
		}
		case MODE_LIGHTSENSOR_READ: {
			if (!adc_read_mode) {
				// set the led and step to first step
				adc_read_mode = 0;
				adc_read_step = 0;
				rgbled_sensor_read_idx(comm_data[0]);
				adc_read_mode = ADC_MODE_LIGHTSENSOR;
			}
			
			// now we are ready to read.
			// data will probably be prior data unless the host waits.
			comm_timeout = 0;
			break;
		}
		case MODE_SLEEP: {
			// put this bitch to bed.
			// we'll wake up when we get another I2C command.
			pirate_sleep_mode = SLEEP_MODE_PWR_DOWN;
			
			comm_cmd = 0;
			comm_timeout = 0;
			break;
		}
		
		// extended commands
		case MODE_AUX_PIN_SET: {
			// TODO: come up with an implementation that isn't shitty
		}
		
		case MODE_AUX_PIN_GET: {
			// TODO: implement when response sending is ...implemented
			comm_cmd = 0;
			comm_timeout = 0;
			break;
		}
		case MODE_LIGHTSENSOR_SENS: { 		// 0x10 0x19 <led 0-3> <sens 1-200>
			if (comm_data_idx >= 1) {
				if ((comm_data[0] <= 0x03) && (comm_data[1])) {
					rgbled_sensor_sensitivity(comm_data[0], comm_data[1]);
				}
				comm_cmd = 0;
				comm_timeout = 0;
			}
			break;
		}
		
		// invalid commands
		default: {
			// send NAK
			comm_cmd = 0;
			comm_timeout = 0;
			return 1;
		}
	}
	
	return 0;
}

static uint8_t pegleg_data_rx(uint8_t data)
{
	if (comm_cmd == MODE_NONE) {
		// we aren't processing any commands, so this must be a new command/request.
		// first 4 bits = command
		comm_cmd = data >> 4;
		// last 4 bits = optional data
		comm_data[0] = data & 0x0f;
	
		// clear our data watchdog and data index
		comm_data_idx = 0;
		comm_timeout = 0;
	
		// if this is an immediate command, process it
		if (data & 0x80) {
			return pegleg_cmd();
		}
	} else {
		// command in progress - add data and continue
		comm_data_idx++;
		comm_data_idx &= (COMM_DATA_SIZE - 1);		// data will LOOP in case of problems; TODO: NAK on failure
		comm_data[comm_data_idx] = data;
		return pegleg_cmd();
	}
	
	return 0;
}

#include "i2c_interrupt.h"


/* ISR handlers */
ISR(TIMER0_COMPA_vect)
{		
	/****
	 * this mainline loop runs at 2KHz, or 0.5ms.
	 * at the faster speed, this is ~3968 cycles/loop @8MHz.
	 ****/
	
	/* TIMEKEEPING */
	// we only count a total of 10 seconds this way before we loop.
	tim0_milli++;
	if (tim0_milli >= 200) {
		tim0_milli = 0;
		tim0_centi++;
		if (tim0_centi >= 100) {
			tim0_centi = 0;
		}
	}
	
	// 2khz fix. our timer isn't evenly divisible to get our 2K, so we do this...
	OCR0A = (OCR0A == TIMER0_COMPARE) ? TIMER0_COMPARE + 1 : TIMER0_COMPARE;	
	
	
	/* ADC / LIGHT SENSOR / TEMP SENSOR */
	// main adc handler
	if (adc_read_mode) {
		switch (adc_read_mode) {
			case ADC_MODE_TEMPSENSOR: {
				tempsensor_read();
				break;
			}
			case ADC_MODE_LIGHTSENSOR: {
				rgbled_sensor_read();
				break;	
			}
		}
		
		if (adc_read_step <= 0xfe) {
			adc_read_step++;
		}
	} /* else {
		if (adc_read_step != 0xff) {
			adc_read_step++;
			if (adc_read_step == 0xff) {
				// disable the ADC
				ADCSRA &= ~(_BV(ADEN));
			}
		}
	} */ // NOTE: we need to keep the ADC enabled; it takes too long to warm up.
	
	
	/* LEDs */
	// TODO: test common cathode mode
	// figure out next LED to update (order is always RGB(X) repeating)
	if (adc_read_mode != ADC_MODE_LIGHTSENSOR) {
		rgbled_idx++;
#ifdef LED_RGBX_4LED
		rgbled_idx &= 0x03;
#else
		if (rgbled_idx > 2) {
			rgbled_idx = 0;
		}
#endif

		// and now update them
		rgbled_update();
	}
	
	
	/* COMMAND IO */
	// stop processing the command if it isn't complete in 100ms.
	if (comm_cmd != MODE_NONE) {
		if (++comm_timeout >= 50) {
			comm_cmd = MODE_NONE;
			comm_timeout = 0;
			i2c_disable_slave();
			i2c_enable_slave();
		}
	}
	
	/* PROFILING */
	// determine how much CPU usage we are consuming
	tim0_profiler = TCNT0;
}

