/*
 * Switch Card firmware to Amtega8.
 *
 * Copyright (C) 2011 Otvos Attila oattila@onebithq.com
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with MPlayer; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <stdlib.h>
#include "avr/io.h"
#include "avr/interrupt.h"
#include <avr/pgmspace.h>
#include <util/delay.h>
#include <avr/eeprom.h>

#define RX_TIMEOUT      20

//OUT:
// 1 - PB0
// 2 - PD7
// 3 - PD6
// 4 - PD5
// 5 - PD4
// 6 - PD3
//
// IN:
// 1 - PC0
// 2 - PC1
// 3 - PC2
// 4 - PC3
// 5 - PC4
// 6 - PC5

#define SW0                             0x01
#define SW1                             0x02
#define SW2                             0x04
#define SW3                             0x08
#define SW4                             0x10
#define SW5                             0x20

#define DEVICEID                        0x00
#define OUTPUTCONF                      0x01

#define CMD_BUFFER_SIZE                 16
#define OUTPUTNUMBER                    6

#define OUTPUTTYPE_SWITCH               0x00
#define OUTPUTTYPE_OUTPUT               0x01
#define OUTPUTTYPE_ALTER                0x02
#define OUTPUTTYPE_FLIPFLOP_UP          0x03
#define OUTPUTTYPE_FLIPFLOP_DOWN        0x04
#define OUTPUTTYPE_TIMEOUT_UP_RESTART   0x05
#define OUTPUTTYPE_TIMEOUT_DOWN_RESTART 0x06
#define OUTPUTTYPE_TIMEOUT_UP_RESET     0x07
#define OUTPUTTYPE_TIMEOUT_DOWN_RESET   0x08
#define OUTPUTTYPE_TIMEOUT_UP_ONCE      0x09
#define OUTPUTTYPE_TIMEOUT_DOWN_ONCE    0x0a

#define OUTPUTTYPE_MASK                 0x0f

#define MAGIC_CHAR                      '*'

static volatile unsigned char b_port;
static volatile unsigned char c_port;

static volatile unsigned char TxEnabled;
static volatile unsigned char Magic;
static volatile unsigned char Cmd;
static volatile unsigned char CmdPar[CMD_BUFFER_SIZE];
static volatile unsigned char deviceid;
static volatile unsigned char error;

static volatile unsigned char b_port_last;
static volatile unsigned char b_port_stat;
static volatile unsigned char c_port_last;
static volatile unsigned char c_port_stat;
static volatile unsigned char output_stat;

static volatile unsigned char sended_switch;
static volatile unsigned char sended_output;
static volatile unsigned int stime;
static volatile unsigned int dtime;
static volatile unsigned int ftime;
static volatile unsigned char forcesend;
static volatile unsigned char sendproc;
static volatile unsigned char collisionptr;
static volatile unsigned char outputconfsend;

typedef struct {
    unsigned char deviceid;
    unsigned char inputid;
    unsigned char type;
    unsigned char par;
} output_conf_t;

static volatile output_conf_t output_conf[OUTPUTNUMBER];
static volatile char input_stat[OUTPUTNUMBER];
static volatile unsigned int output_timer[OUTPUTNUMBER];

#define BUFFER_SIZE     32
#define BUFFER_MASK     (BUFFER_SIZE-1)
#define RX_NO_DATA      0x0100
#define RX_OVERFLOW     0x0200
#define TX_COLLISION    0x1000

static volatile char RxBuffer[BUFFER_SIZE];
static volatile char RxError;
static volatile int  RxPtr;
static volatile int  RxDataPtr;
static volatile int  RxCmdFlag;

static volatile char TxBuffer[BUFFER_SIZE];
static volatile char TxError;
static volatile int  TxPtr;
static volatile int  TxDataPtr;
static volatile int  TxCheckPtr;
static volatile int  TxAddPtr;

SIGNAL(SIG_UART_RECV) {
    unsigned char status;
    unsigned char data;
    unsigned int newPtr;

    status = UCSRA;
    data = UDR;
    RxError = status & (_BV(FE)|_BV(DOR));
    newPtr = (RxPtr + 1) & BUFFER_MASK;
    if(newPtr == RxDataPtr) {
        RxError = RX_OVERFLOW >> 8;
        RxCmdFlag++;
        return;
    }
    if(TxCheckPtr<TxDataPtr && TxDataPtr) {
        if(TxBuffer[TxCheckPtr++]!=data) {
            UCSRB &= ~_BV(UDRIE);
            PORTD &= ~(1<<PIND2);
            TxEnabled = 0;
            TxDataPtr=0;
            TxPtr=0;
            TxCheckPtr=0;
            RxCmdFlag++;
            TxError = TX_COLLISION >> 8;
        }
    }
    RxPtr=newPtr;
    RxBuffer[RxPtr]=data;
    if(data=='\n') {
        RxCmdFlag++;
    }
}

SIGNAL(SIG_UART_DATA) {

    if(TxPtr>=TxDataPtr || TxPtr>=BUFFER_SIZE) {
        UCSRB &= ~_BV(UDRIE);
        TxPtr=TxDataPtr;
        return;
    }
    UDR = TxBuffer[TxPtr++];
}

void init_uart() {
    unsigned int baudrate;

    RxError = 0;
    RxPtr = 0;
    RxDataPtr = 0;
    RxCmdFlag = 0;
    TxError = 0;
    TxPtr = 0;
    TxDataPtr = 0;
    TxCheckPtr = 0;
    TxAddPtr = 0;
    baudrate = (F_CPU)/((UART_BAUD)*16l)-1;

    UBRRH = (unsigned char)(baudrate>>8);
    UBRRL = (unsigned char)(baudrate);
    UCSRB = _BV(RXCIE)|(1<<RXEN)|(1<<TXEN);
#ifdef URSEL
    UCSRC = (1<<URSEL)|(3<<UCSZ0);
#else
    UCSRC = (3<<UCSZ0);
#endif

    int i;
    for(i=0;i<BUFFER_SIZE;i++)
        TxBuffer[i]='.';
}

int isRxEmpty(void) {
    return RxPtr==RxDataPtr;
}

int isTxEmpty(void) {
    return TxPtr==TxDataPtr;
}

int isRxCmd(void) {
    int ret;

    ret=RxCmdFlag;
    if(RxCmdFlag)
        RxCmdFlag--;
    return ret;
}

void dropTxData(void) {
    UCSRB &= ~_BV(UDRIE);
    TxDataPtr=0;
    TxPtr=0;
    TxCheckPtr=0;
    TxAddPtr=0;
}

unsigned int getRxData(void) {
    unsigned char data;
    unsigned int newPtr;

    if(RxDataPtr==RxPtr) {
        RxDataPtr=0;
        RxPtr=0;
        return RX_NO_DATA;
    }
    newPtr=(RxDataPtr+1) & BUFFER_MASK;
    RxDataPtr=newPtr;
    data=RxBuffer[RxDataPtr];
//    return data;
    return (RxError << 8) + data;
}

void addTxData(unsigned char c) {
    while(TxPtr!=TxDataPtr) {};
    UCSRB &= ~_BV(UDRIE);
    if(TxAddPtr>=BUFFER_SIZE)
        return;
    TxBuffer[TxAddPtr++]=c;
}

void addTxHalfByte(unsigned char c) {
    unsigned char h;
    h=c&0xf;
    if(h>9)
        h+=('A'-10);
    else
        h+='0';
    addTxData(h);
}

void addTxHex(unsigned char c) {
    addTxHalfByte(c>>4);
    addTxHalfByte(c);
}

void commitTxData(void) {
    while(TxPtr!=TxDataPtr) {};
    UCSRB &= ~_BV(UDRIE);
    TxPtr=0;
    TxDataPtr=TxAddPtr;
    TxCheckPtr=0;
    TxAddPtr=0;
    UCSRB |= _BV(UDRIE);
}

void delay(double msec) {
    unsigned int _msec = msec;
    _delay_ms(msec);
    stime+=_msec;
    dtime+=_msec;
    if(_msec<ftime) {
        ftime-=_msec;
    } else if(ftime) {
        RxCmdFlag++;
        ftime=0;
    }
}

void set_output(const unsigned char _output_stat) {
    unsigned char o = PORTD;

    o &= ~((1<<PIND7) | (1<<PIND6) | (1<<PIND5) | (1<<PIND4) | (1<<PIND3));
    if(_output_stat & SW0)
        PORTB |= (1<<PINB0);
    else
        PORTB &= ~(1<<PINB0);
    if(_output_stat & SW1)
        o |= (1<<PIND7);
    if(_output_stat & SW2)
        o |= (1<<PIND6);
    if(_output_stat & SW3)
        o |= (1<<PIND5);
    if(_output_stat & SW4)
        o |= (1<<PIND4);
    if(_output_stat & SW5)
        o |= (1<<PIND3);
    PORTD = o;
}

int send_header(unsigned char id) {
    TxEnabled = 1;
    PORTD |= (1<<PIND2);                // EN = 1
    delay(2);
    dropTxData();
    if(!isTxEmpty()) {
        PORTD &= ~(1<<PIND2);               // EN = 0;
        TxEnabled = 0;
        dropTxData();
        return 1;
    }
    addTxData('\n');
    addTxData(MAGIC_CHAR);
    addTxData(id);
    addTxHex(deviceid);
    return 0;
}

void send_error() {
    if(send_header('E'))
        return;
    addTxHex(error);
    addTxData('\n');
    error=0;
    commitTxData();
}

void send_status() {
    if(send_header('S'))
        return;
    addTxHex(c_port_stat);
    addTxHex(output_stat);
    addTxData('\n');
    collisionptr++;
    if(collisionptr>8)
        collisionptr=1;
    sendproc=1;
    commitTxData();
}

void send_output(const unsigned char n) {
    if(send_header('O'))
        return;
    addTxHex(n);
    addTxHex(output_conf[n].deviceid);
    addTxHex(output_conf[n].inputid);
    addTxHex(output_conf[n].type);
    addTxHex(output_conf[n].par);
    addTxData('\n');
    commitTxData();
}

static unsigned char hex2dec(const unsigned char c) {
    if(c<'0')
        return 0;
    if(c<='9')
        return c-48;
    if(c<'A')
        return 0;
    if(c<='F')
        return c-55;
    if(c<'a')
        return 0;
    if(c>'f')
        return 0;
    return c-87;
}

unsigned int calc_timer(unsigned char t) {
    unsigned int ret;

    if(t>60) {
        ret = 60+(t-60)*10;
    } else
        ret = t;
    return ret;
}

void process_input(const unsigned char id, const unsigned char input, const unsigned char output) {
    unsigned char i;
    unsigned char t;
    unsigned char outputmask;
    unsigned char inputmask;
    unsigned char altermask;
    unsigned char timerdec = 0;
    unsigned char edgeup=0;
    unsigned char edgedown=0;

    if(stime>=500) {       // 1 sec
        stime-=500;
        if((b_port_stat & (1<<(PINB2))) && deviceid!=0xff)
            forcesend=1;
    }
    if(dtime>=1000) {       // 5 sec
        dtime-=1000;
        timerdec = 1;
    }
    for(i=0;i<OUTPUTNUMBER;i++) {
        outputmask = 1 << i;
        if(timerdec && output_timer[i]) {
            output_timer[i]--;
            if(!output_timer[i])
                output_stat &= ~outputmask;
        }
        if(output_conf[i].deviceid!=id)
            continue;
        inputmask = 1 << output_conf[i].inputid;
        t=output_conf[i].type & OUTPUTTYPE_MASK;

        edgeup=((input & inputmask)!=input_stat[i] && !input_stat[i]);
        edgedown=((input & inputmask)!=input_stat[i] && input_stat[i]);

        switch(t) {
        case OUTPUTTYPE_SWITCH:
            output_stat &= ~outputmask;
            if(input & inputmask)
                output_stat |= outputmask;
            break;
        case OUTPUTTYPE_OUTPUT:
            output_stat &= ~outputmask;
            if(output & inputmask)
                output_stat |= outputmask;
            break;
        case OUTPUTTYPE_ALTER:
            if(output_conf[i].par<OUTPUTNUMBER) {
                altermask = 1 << output_conf[i].par;
                output_stat &= ~outputmask;
                if(((input & inputmask) && !(c_port_stat & altermask)) || (!(input & inputmask) && (c_port_stat & altermask)))
                    output_stat |= outputmask;
            }
            break;
        case OUTPUTTYPE_FLIPFLOP_UP:
            if(edgeup)
                output_stat ^= outputmask;
            break;
        case OUTPUTTYPE_FLIPFLOP_DOWN:
            if(edgedown)
                output_stat ^= outputmask;
            break;
        case OUTPUTTYPE_TIMEOUT_UP_RESTART:
            if(edgeup) {
                output_stat |= outputmask;
                output_timer[i]=calc_timer(output_conf[i].par);
            }
            break;
        case OUTPUTTYPE_TIMEOUT_DOWN_RESTART:
            if(edgedown) {
                output_stat |= outputmask;
                output_timer[i]=calc_timer(output_conf[i].par);
            }
            break;
        case OUTPUTTYPE_TIMEOUT_UP_RESET:
            if(edgeup) {
                if(output_timer[i]) {
                    output_stat &= ~outputmask;
                    output_timer[i]=0;
                } else {
                    output_stat |= outputmask;
                    output_timer[i]=calc_timer(output_conf[i].par);
                }
            }
            break;
        case OUTPUTTYPE_TIMEOUT_DOWN_RESET:
            if(edgedown) {
                if(output_timer[i]) {
                    output_stat &= ~outputmask;
                    output_timer[i]=0;
                } else {
                    output_stat |= outputmask;
                    output_timer[i]=calc_timer(output_conf[i].par);
                }
            }
            break;
        case OUTPUTTYPE_TIMEOUT_UP_ONCE:
            if(edgeup) {
                if(!output_timer[i]) {
                    output_stat |= outputmask;
                    output_timer[i]=calc_timer(output_conf[i].par);
                }
            }
            break;
        case OUTPUTTYPE_TIMEOUT_DOWN_ONCE:
            if(edgedown) {
                if(!output_timer[i]) {
                    output_stat |= outputmask;
                    output_timer[i]=calc_timer(output_conf[i].par);
                }
            }
            break;
        }
        input_stat[i]=input & inputmask;
    }
}

static void read_inputs() {
    unsigned char smask = 0;

    c_port = PINC & (1<<(PINC0)|1<<(PINC1)|1<<(PINC2)|1<<(PINC3)|1<<(PINC4)|1<<(PINC5));
    b_port = PINB & (1<<(PINB2)|1<<(PINB3)|1<<(PINB4));

    smask = b_port ^ b_port_last;
    b_port_last = b_port;
    b_port_stat &= (smask ^ 0xff);
    b_port &= smask;
    b_port_stat |= b_port;

    smask = c_port ^ c_port_last;
    c_port_last = c_port;
    c_port_stat &= (smask ^ 0xff);
    c_port &= smask;
    c_port_stat |= c_port;
}

void manual_config(void) {
    int n = 0;
    unsigned char c = 0;
    unsigned char d;
    unsigned char b;
    unsigned char e = (1<<PINB3) | (1<<PINB4);

    d = deviceid;
    read_inputs();
    delay(10);
    read_inputs();
    if((b_port_stat & e) == 0x00) {
        PORTB &= ~(1<<PINB1);
        while(1) {
            delay(1);
            read_inputs();
            if((b_port_stat & e)==e)
                break;
        }
        c=1;
        if(d>0x3f)
            d=0x3f;
    }
    b=b_port_stat;
    while(c) {
        delay(10);
        read_inputs();
        n++;
        if(n>25) {
            if((deviceid & 0x3f)==d) {
                c ^= 2;
            }
            n=0;
        }
        if((deviceid & 0x3f)!=d)
            c &= ~2;
        if(c & 2) {
            output_stat = 0;
        } else {
            output_stat = d & 0x3f;
        }
        set_output(output_stat);

        if(((b_port_stat & (1<<PINB3))!=(b & (1<<PINB3))) && !(b & (1<<PINB3)))
            d--;
        if(((b_port_stat & (1<<PINB4))!=(b & (1<<PINB4))) && !(b & (1<<PINB4)))
            d++;
        b=b_port_stat;

        if(d<0x01)
            d=0x3f;
        if(d>0x3f)
            d=0x01;

        if((b_port_stat & e) == 0x00) {
            while((b_port_stat & e)!=e) {
                delay(1);
                read_inputs();
            }
            if(d==0x3f)
                d=0xff;
            if(d!=deviceid) {
                eeprom_write_byte(DEVICEID, d);
                deviceid=d;
            }
            c=0;
        }
    }
    output_stat = 0;
    error=0;
    set_output(output_stat);
}

void process_command(void) {
    unsigned int res;
    int i;
    int n = 0;
    unsigned char d = 0;
    unsigned char c;

    ftime=0;
    PORTB ^=  (1<<PINB1);
    i=0;
    n=0;
    res = getRxData();
    Magic=0;
    while(res!=RX_NO_DATA) {
        c = res;
        if(!(res & 0xff00)) {
            if(!i)
                Magic=c;
            else if(i==1)
                Cmd=c;
            else {
                if(i&0x01) {
                    d|=hex2dec(c);
                    CmdPar[n++]=d;
                } else {
                    d=hex2dec(c)<<4;
                }
            }
        } else
            break;
        if(c=='\n')
            break;
        if(Magic=='*' || Magic=='#')
            i++;
        res = getRxData();
    }

    if(Magic!=MAGIC_CHAR)
        return;
    if(n==3 && Cmd=='S' && CmdPar[0]!=deviceid) {
        process_input(CmdPar[0],CmdPar[1],CmdPar[2]);
    } else if(n && CmdPar[0]==deviceid) {
        switch(Cmd) {
        case 'S':
            if(n==3 && !TxError) {
                if(sended_switch==c_port_stat && sended_output==output_stat)
                    forcesend=0;
                else
                    forcesend=1;
                sended_switch=CmdPar[1];
                sended_output=CmdPar[2];
                collisionptr=0;
                if(sended_switch==c_port_stat && sended_output==output_stat) {
                    sendproc=0;
                    stime=0;
                }
            }
            break;
        case 'G':
            if(n==1) {
                forcesend=1;
            } else {
                error=0x01;
            }
            break;
        case 'Q':
            if(CmdPar[1]<OUTPUTNUMBER && n==2) {
                outputconfsend=CmdPar[1];
            } else {
                error=0x01;
            }
            break;
        case 'L':
            if(CmdPar[1]<OUTPUTNUMBER && n==6) {
                output_conf[CmdPar[1]].deviceid=CmdPar[2];
                output_conf[CmdPar[1]].inputid=CmdPar[3];
                output_conf[CmdPar[1]].type=CmdPar[4];
                output_conf[CmdPar[1]].par=CmdPar[5];
                eeprom_write_block(&output_conf, (unsigned char*)OUTPUTCONF, sizeof(output_conf));
                outputconfsend=CmdPar[1];
            } else {
                error=0x01;
            }
            break;
        case 'D':
            if(n==2) {
                if(CmdPar[1]!=deviceid && (b_port_stat & (1<<(PINB2)))) {
                    deviceid=CmdPar[1];
                    eeprom_write_byte(DEVICEID, deviceid);
                }
                forcesend=1;
            } else {
                error=0x01;
            }
            break;
        }
        delay(4);
    }
}

//-------------------------------------
int main(void) {
//-------------------------------------
    DDRB = (1<<PINB1)|(1<<PINB0);
    DDRC = 0x00;
    DDRD = (1<<PIND2)|(1<<PIND3)|(1<<PIND4)|(1<<PIND5)|(1<<PIND6)|(1<<PIND7);

    PORTB = (1<<PINB1) |(1<<PINB2) | (1<<PINB3) | (1<<PINB4);
    PORTC = 0x00;
    PORTD = 0x00;

    b_port = 0;
    b_port_stat = 0;
    b_port_last = 0;
    c_port = 0;
    c_port_stat = 0;
    c_port_last = 0;
    TxEnabled = 0;
    output_stat = 0;
    sended_switch = 0xff;
    sended_output = 0xff;
    stime = 0;
    dtime = 0;
    ftime = 0;
    forcesend=0;
    sendproc=0;
    outputconfsend=0xff;

    int i;
    unsigned char b;
    unsigned char cmask;
    unsigned char outtest = 0;

    while(!eeprom_is_ready()) {};
    deviceid = eeprom_read_byte (DEVICEID);
    eeprom_read_block(&output_conf, (unsigned char*)OUTPUTCONF, sizeof(output_conf));

    init_uart();
    sei();

    manual_config();

    PORTB |= (1<<PINB1);
    PORTD &= ~(1<<PIND2);               // EN = 0;

    b=b_port_stat;
    while(1) {
        read_inputs();

        process_input(deviceid, c_port_stat, output_stat);

        if(((b_port_stat & (1<<PINB4))!=(b & (1<<PINB4))) && !(b & (1<<PINB4))) {
            if(b & (1<<PINB3)) {
                forcesend=1;
            } else {
                outtest++;
                if(outtest>7)
                    outtest=1;
            }
        }
        if(((b_port_stat & (1<<PINB3))!=(b & (1<<PINB3))) && (b & (1<<PINB3))) {
            if(!outtest && (b & (1<<PINB4)))
                outtest=7;
        }
        if(((b_port_stat & (1<<PINB3))!=(b & (1<<PINB3))) && !(b & (1<<PINB3))) {
            outtest=0;
        }
        b=b_port_stat;

        if(!isRxEmpty() && !ftime) {
            ftime=RX_TIMEOUT;
        }

        while(isRxCmd())
            process_command();

        if(isRxEmpty()) {
            if(error) {
                send_error();
            } else if(outputconfsend!=0xff) {
                send_output(outputconfsend);
                outputconfsend=0xff;
            } else if(sended_switch!=c_port_stat || sended_output!=output_stat || forcesend || sendproc) {
                send_status();
            }
        }
        if(TxError) {
            TxError=0;
            if(!collisionptr)
                collisionptr=1;
        }
        if(isTxEmpty() && TxEnabled) {
            delay(2);
            PORTD &= ~(1<<PIND2);               // EN = 0;
            TxEnabled=0;
        }
        if(outtest) {
            if(outtest==7)
                set_output(0x3f);
            else
                set_output((1<<(outtest-1)));
        } else {
            set_output(output_stat);
        }
        if(collisionptr) {
            delay(2);
            for(i=0;i<collisionptr;i++)
                delay(2);
            cmask=(1<<(collisionptr-1));
            if(cmask&deviceid)
                delay(2);
        } else
            delay(1);
    }
}
