/*
    Lora 2.1 FPGA and GPS reset utility
*/


#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>


/* I2C I/O expander specification (BSP) */
#define I2C_DEVICE                 "/dev/i2c-0"
#define I2CADDR_PORTEXPANDER        0x20
#define I2CADDR_EXT_PORTEXPANDER    0x21

#define PORTEXPANDER_INPUT_PORT0_REG    0x00
#define PORTEXPANDER_INPUT_PORT1_REG    0x01
#define PORTEXPANDER_OUTPUT_PORT0_REG   0x02
#define PORTEXPANDER_OUTPUT_PORT1_REG   0x03
#define PORTEXPANDER_CONFIG_PORT0_REG   0x06
#define PORTEXPANDER_CONFIG_PORT1_REG   0x07

#define FPGA_nCE_PORT               0
#define FPGA_nCONFIG_PORT           1
#define FPGA_GPS_RESET_PORT         3
#define FPGA_CONF_DONE_PORT         5
#define FPGA_EXT_CONF_DONE_PORT     6

#define CPU_EPCS_EN_PORT            0
#define EXT_EPCS_EN_PORT            1

#define MAX_FPGA_LOADING_TIMER      10

int global_i2c_fd = -1;

int i2c_open( uint8_t i21c_addr )
{
    global_i2c_fd = open(I2C_DEVICE, O_RDWR);
    if( ioctl( global_i2c_fd, I2C_SLAVE, i21c_addr ) < 0 )
    {
        fprintf(stderr,"ERROR: Failed to acquire bus access and/or talk to slave - %s\n", strerror(errno) );
        close(global_i2c_fd);
        return -1;
    }
    return 0;
}

void i2c_close( void )
{
    if(global_i2c_fd != -1)
        close(global_i2c_fd);
    global_i2c_fd = 1;
}

void i2c_read( uint8_t i21c_addr, uint8_t reg_addr, uint8_t *bval )
{
    struct i2c_msg msg[2];
    struct i2c_rdwr_ioctl_data b;
    uint8_t *inbuf, outbuf;

    outbuf = reg_addr;
    msg[0].addr = i21c_addr;
    msg[0].flags= 0;
    msg[0].len = sizeof(outbuf);
    msg[0].buf = &outbuf;

    inbuf = bval;
    msg[1].addr = i21c_addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = sizeof(*inbuf);
    msg[1].buf = inbuf;

    b.msgs = msg;
    b.nmsgs = 2;

    if( ioctl(global_i2c_fd, I2C_RDWR, &b) < 0 )
    {
        fprintf(stderr,"ERROR: Read from I2C Device failed (%d, 0x%02x, 0x%02x) - %s\n", global_i2c_fd, i21c_addr, reg_addr, strerror(errno) );
    }
}

void i2c_write( uint8_t i21c_addr, uint8_t reg_addr, uint8_t bval )
{
    unsigned char buff[2];
    struct i2c_rdwr_ioctl_data b;
    struct i2c_msg msg[1];

    buff[0] = reg_addr;
    buff[1] = bval;

    msg[0].addr = i21c_addr;
    msg[0].flags = 0;
    msg[0].len = sizeof(buff);
    msg[0].buf = buff;

    b.msgs = msg;
    b.nmsgs = 1;

    if( ioctl(global_i2c_fd, I2C_RDWR, &b) < 0 )
    {
        fprintf(stderr,"ERROR: Write to I2C Device failed (%d, 0x%02x, 0x%02x) - %s\n", global_i2c_fd, i21c_addr, reg_addr, strerror(errno) );
        return;
    }

}

void usage ()
{
    fprintf(stderr, "LoRa Reset utility:\n" );
    fprintf(stderr, "  -h  : This help\n" );
    fprintf(stderr, "  -b  : Select board [0,1]\n" );
    fprintf(stderr, "  -g  : GPS reset\n");
    fprintf(stderr, "  -s  : Sleep time [0-99999]  -- default is 1 second\n");
    fprintf(stderr, "  -f  : FPGA reset\n" );
    fprintf(stderr, "Either -f or -g or both is required\n" );
}

int main( int argc, char ** argv )
{
    int i, err;
    uint8_t val = 0;
    uint8_t opt_stime = 1;
    uint8_t opt_reset_fpga = 0, opt_gps_reset = 0;
    uint8_t board = 0, i2c_expander_addr = I2CADDR_PORTEXPANDER, conf_done_port = FPGA_CONF_DONE_PORT, input_port_reg = PORTEXPANDER_INPUT_PORT0_REG;
    int timeout = 0;
    
    if( argc < 2 )
    {
        usage( );
        return EXIT_FAILURE;
    }

    /* Parse command line options */
    while( (i = getopt( argc, argv, "b:fghs:" )) != -1 )
    {
        switch( i )
        {
        case 'g':
            opt_gps_reset = 1;
            break;
        case 'h':
            usage( );
            return EXIT_SUCCESS;
            break;
        case 's':
            opt_stime = atoi(optarg);
            break;
        case 'f':
            opt_reset_fpga = 1;
            break;
        case 'b':
            board = atoi(optarg);
            fprintf(stderr, "Board %d\n", board );
            break;

        default:
            fprintf(stderr, "ERROR: argument parsing options\n" );
            usage( );
            return EXIT_FAILURE;
        }
    }

    if (!(opt_reset_fpga || opt_gps_reset)) {
        fprintf(stderr,"Nothing to do!\n");
        exit(0);
    }
    
    switch(board)
    {
        case 0:
            i2c_expander_addr = I2CADDR_PORTEXPANDER;
            conf_done_port = FPGA_CONF_DONE_PORT;
            input_port_reg = PORTEXPANDER_INPUT_PORT0_REG;
            break;
        case 1:
            i2c_expander_addr = I2CADDR_EXT_PORTEXPANDER;
            conf_done_port = FPGA_EXT_CONF_DONE_PORT;
            input_port_reg = PORTEXPANDER_INPUT_PORT1_REG;
            break;
        default:
            fprintf(stderr, "ERRROR: no board %d\n", board );
            return EXIT_FAILURE;
    }

    /* Open the I2C port expander */
    fprintf(stderr, "Opening the I2C Port expander\n" );
    err = i2c_open( I2CADDR_PORTEXPANDER );
    if ( err != 0 )
    {
        fprintf(stderr, "ERRROR: failed to open I2C device (err=%i)\n", err );
        return EXIT_FAILURE;
    }

    /* Configuration of the I2C port expander */
    i2c_read( I2CADDR_PORTEXPANDER, PORTEXPANDER_CONFIG_PORT0_REG, (uint8_t *)&val );
    fprintf(stderr, "I2C expander Port 0 config initial state: register: 0x%02x\n", val );
    if (opt_reset_fpga) {
        val &= ~(1 << FPGA_nCE_PORT);        /* CE_PORT as output */
        val &= ~(1 << FPGA_nCONFIG_PORT);    /* CONFIG_PORT as output */
        val |=  (1 << FPGA_CONF_DONE_PORT);  /* CONFIG_DONE as input */
    }
    if (opt_gps_reset)
        val &= ~(1<<FPGA_GPS_RESET_PORT); /* GPS Reset Port as input */
    
    fprintf(stderr, "I2C expander Port 0 config register: 0x%02x\n", val );
    i2c_write( I2CADDR_PORTEXPANDER, PORTEXPANDER_CONFIG_PORT0_REG, val );

    /* Set port CPU_EPCS_EN and EXT_EPCS_EN as an OUTPUT */
    i2c_read( I2CADDR_PORTEXPANDER, PORTEXPANDER_CONFIG_PORT1_REG, &val );
    val &= ~(1 << CPU_EPCS_EN_PORT);    /* CPU_EPCS_EN_PORT as output */
    val &= ~(1 << EXT_EPCS_EN_PORT);    /* EXT_EPCS_EN_PORT as output */
    val |=  (1 << FPGA_EXT_CONF_DONE_PORT);  /* CONFIG_DONE as input */
    i2c_write( I2CADDR_PORTEXPANDER, PORTEXPANDER_CONFIG_PORT1_REG, val );
    i2c_read( I2CADDR_PORTEXPANDER, PORTEXPANDER_CONFIG_PORT1_REG, &val );
    fprintf(stderr, "I2C expander Port 1 config register: 0x%02x\n", val );
    /* Set port CPU_EPCS_EN or EXT_EPCS_EN output level to 1 */
    i2c_read( I2CADDR_PORTEXPANDER, PORTEXPANDER_OUTPUT_PORT1_REG, &val );
    val |= 0x03;
    i2c_write( I2CADDR_PORTEXPANDER, PORTEXPANDER_OUTPUT_PORT1_REG, val );
    i2c_read( I2CADDR_PORTEXPANDER, PORTEXPANDER_OUTPUT_PORT1_REG, &val);
    fprintf(stderr, "I2C expander Port 1 output level: 0x%02x\n", val );


    if( board == 1)
    {
        /* Close the I2C port expander */
        i2c_close();

        /* Open the I2C driver */
        fprintf(stderr, "Opening the I2C External Port expander\n" );
        err = i2c_open( I2CADDR_EXT_PORTEXPANDER );
        if ( err != 0 )
        {
            fprintf(stderr, "ERRROR: failed to open I2C device (err=%i)\n", err );
            return EXIT_FAILURE;
        }
    }
            
    if( opt_gps_reset )
    {
        /* GPS RESET PORT HIGH */
        i2c_read( i2c_expander_addr, PORTEXPANDER_CONFIG_PORT0_REG, &val );
        fprintf(stderr, "GPS CONFIG_PORT HIGH\n" );
        val |= (1 << FPGA_GPS_RESET_PORT);
        i2c_write( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, val );
    }
    
    if(opt_reset_fpga)
    {
        /* Reboot procedure of the FPGA */
        /* nCE LOW */
        i2c_read( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, &val );
        fprintf(stderr, "I2C nCE LOW\n" );
        val &= ~(1 << FPGA_nCE_PORT);
        i2c_write( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, val );
    }
    sleep( opt_stime );

    if( opt_gps_reset )
    {
        /* GPS RESET PORT LOW */
        i2c_read( i2c_expander_addr, PORTEXPANDER_CONFIG_PORT0_REG, &val );
        fprintf(stderr, "GPS CONFIG_PORT LOW\n" );
        val &= ~(1 << FPGA_GPS_RESET_PORT);
        i2c_write( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, val );
    }

    if(opt_reset_fpga)
    {
        /* FPGA_PORT LOW */
        i2c_read( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, &val );
        fprintf(stderr, "I2C CONFIG_PORT LOW\n" );
        val &= ~(1 << FPGA_nCONFIG_PORT);
        i2c_write( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, val );
    }
    
    sleep( opt_stime );

    if( opt_gps_reset )
    {
        /* GPS RESET PORT HIGH */
        i2c_read( i2c_expander_addr, PORTEXPANDER_CONFIG_PORT0_REG, &val );
        fprintf(stderr, "GPS CONFIG_PORT HIGH\n" );
        val |= (1 << FPGA_GPS_RESET_PORT);
        i2c_write( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, val );
    }

    if(opt_reset_fpga)
    {
        /* FPGA_PORT HIGH */
        i2c_read( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, &val );
        fprintf(stderr, "I2C CONFIG_PORT HIGH\n" );
        val |= (1 << FPGA_nCONFIG_PORT);
        i2c_write( i2c_expander_addr, PORTEXPANDER_OUTPUT_PORT0_REG, val );
    }

            
    if(board == 1)
    {
        /* Close the external I2C port expander */
        i2c_close();

        /* Open the I2C driver */
        fprintf(stderr, "Opening the I2C Port expander\n" );
        err = i2c_open( I2CADDR_PORTEXPANDER );
        if ( err != 0 )
        {
            fprintf(stderr, "ERRROR: failed to open I2C device (err=%i)\n", err );
            return EXIT_FAILURE;
        }
    }

    if(opt_reset_fpga) {
        /* WAIT for CONFIG_DONE high on the FPGA */
        timeout = 0;
        fprintf(stderr,"input_port_reg %d conf_done_port %d\n",input_port_reg, conf_done_port);
        do
        {
            i2c_read( I2CADDR_PORTEXPANDER, input_port_reg, &val);
            sleep( opt_stime );
            timeout++;
        } while( !(val & (1 << conf_done_port)) && (timeout <= MAX_FPGA_LOADING_TIMER) );
        if( !(val & (1 << conf_done_port) ) )
        {
            fprintf(stderr, "FATAL ERROR: Reboot of the FPGA in failure val %d\n", val );
        }
        else
        {
            fprintf(stderr, "I2C CONFIG_DONE HIGH\n" );
        }
    }

    /* Close the I2C driver */
    i2c_close();
    
    /* Set the port expander back to its initial state */
    fprintf(stderr, "Opening the I2C Port expander\n" );
    err = i2c_open( I2CADDR_PORTEXPANDER );
    if ( err != 0 )
    {
        fprintf(stderr, "ERRROR: failed to open I2C device (err=%i)\n", err );
        return EXIT_FAILURE;
    }
    val = 0xff;
    fprintf(stderr, "Completed reset(s).  Disabling I2C expander Port 0 config register: 0x%02x\n", val );
    i2c_write( I2CADDR_PORTEXPANDER, PORTEXPANDER_CONFIG_PORT0_REG, val );
    i2c_close();

    return 0;
}