/* on ubuntu compile with gcc -W rkusbtool.c -o rkusbtool -lusb-1.0 -I/usr/include/libusb-1.0/ */
#include <libusb.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define VERSION "v0.1"

#define RETRY_MAX 5
#define USB_TIMEOUT 512
#define VENDORID  0x071b
#define PRODUCTID 0x3203

#define OUT_EP  0x01
#define IN_EP   0x82

#define CBW_SIGNATURE 0x43425355
#define CSW_SIGNATURE 0x53425355
#define SCSICMD_READ_12 0xa8

/* rockchip specific commands */
#define RK_CMD            0xe0
#define RK_GET_VERSION    0xffffffff
#define RK_SWITCH_ROCKUSB 0xfeffffff
#define RK_CHECK_USB      0xfdffffff
#define RK_OPEN_SYSDISK   0xfcffffff

enum {
      NONE = 0,
      INFO = 1,
      RKUSB = 2,
      SYSDISK = 4,
      CHECKUSB = 8
};

enum {
      COMMAND_PASSED = 0,
      COMMAND_FAILED = 1,
      PHASE_ERROR    = 2
};

struct CBWCB_t
{
    uint8_t  cbCode;
    uint8_t  cbLun;
    uint32_t LBA;
    uint32_t cbLen;
    uint8_t  reseved;
    uint8_t  control;
} __attribute__((__packed__));

struct CBW_t
{
    uint32_t dCBWSignature;
    uint32_t dCBWTag;
    uint32_t dCBWDataTransferLength;
    uint8_t  bmCBWFlags;
    uint8_t  bCBWLUN;
    uint8_t  bCBWCBLength;
    uint8_t  CBWCB[16];
} __attribute__((__packed__));

struct CSW_t
{
    uint32_t dCSWSignature;
    uint32_t dCSWTag;
    uint32_t dCSWDataResidue;
    uint8_t  bCSWStatus;
} __attribute__((__packed__));

static int send_msc_cmd(libusb_device_handle *hdev, struct CBWCB_t *cbwcb, uint32_t data_len, uint32_t *reftag)
{
    struct CBW_t cbw;
    int ret, repeat, transferred;
    static uint32_t tag = 0xdaefbc01;

    repeat = 0;
    memset(&cbw, 0, sizeof(cbw));
    cbw.dCBWSignature = CBW_SIGNATURE;
    cbw.dCBWTag = tag++;
    cbw.dCBWDataTransferLength = data_len;
    cbw.bmCBWFlags = 0x80; /* device to host */
    cbw.bCBWLUN = 0;
    cbw.bCBWCBLength = sizeof(struct CBWCB_t);
    memcpy(cbw.CBWCB, cbwcb, sizeof(struct CBWCB_t));

    *reftag = cbw.dCBWTag;
    do
    {
        /* transfer command to the device */
        ret = libusb_bulk_transfer(hdev, OUT_EP, (unsigned char*)&cbw, 31, &transferred, USB_TIMEOUT);
        if (ret == LIBUSB_ERROR_PIPE)
        {
            libusb_clear_halt(hdev, OUT_EP);
        }
        repeat++;
    } while ((ret == LIBUSB_ERROR_PIPE) && (repeat < RETRY_MAX));

    if (ret != LIBUSB_SUCCESS)
    {
        printf("error: command transfer error\n");
        return -1;
    }

    return 0;
}

static int get_msc_csw(libusb_device_handle *hdev, uint32_t reftag)
{
    struct CSW_t csw;
    int ret, repeat, transferred;

    /* get CSW response from device */
    repeat = 0;
    do
    {
        ret = libusb_bulk_transfer(hdev, IN_EP, (unsigned char *)&csw, 13, &transferred, USB_TIMEOUT);
        if (ret == LIBUSB_ERROR_PIPE)
        {
            libusb_clear_halt(hdev, IN_EP);
        }
        repeat++;
    } while ((ret == LIBUSB_ERROR_PIPE) && (repeat < RETRY_MAX));

    if (ret != LIBUSB_SUCCESS)
    {
        printf("error reading CSW\n");
        return -3;
    }

    if (transferred != 13)
    {
        printf("error wrong size of CSW packet\n");
        return -4;
    }

    if (csw.dCSWSignature != CSW_SIGNATURE)
    {
        printf("error: wrong CSW signature.\n");
        return -5;
    }

    if (csw.dCSWTag != reftag)
    {
        printf("error: CSW dCSWTag mismatch\n");
        return -6;
    }

    if (csw.bCSWStatus)
    {
        /* In case of CSW indicating error dump the content of the packet */
        printf ("dCSWSignature: 0x%0x\n", csw.dCSWSignature);
        printf ("dCSWTag: 0x%0x\n", csw.dCSWTag);
        printf ("dCSWDataResidue: 0x%0x\n", csw.dCSWDataResidue);
        printf ("bCSWStatus: 0x%0x\n", csw.bCSWStatus);
    }

    return csw.bCSWStatus;
}

static int rk_cmd(libusb_device_handle *hdev, uint32_t command, uint8_t *buf, uint8_t len)
{
    struct CBWCB_t cbwcb;
    int ret, transferred;
    uint32_t reftag;

    /* enter command */
    memset(&cbwcb, 0, sizeof(cbwcb));
    cbwcb.cbCode = SCSICMD_READ_12;
    cbwcb.cbLun = RK_CMD;
    cbwcb.LBA = command; /* RK_GET_VERSION, RK_OPEN_SYSDISK, RK_SWITCH_ROCKUSB */
    cbwcb.cbLen = len;   /* size of transfer in response to this command */

    ret = send_msc_cmd(hdev, &cbwcb, len, &reftag);

    /* get the response */
    if (len > 0)
    {
        ret = libusb_bulk_transfer(hdev, IN_EP, buf, len, &transferred, USB_TIMEOUT);
        if (ret != LIBUSB_SUCCESS || transferred != len)
        {
            printf("error: reading response data failed\n");
            return -2;
        }
    }

    return get_msc_csw(hdev, reftag);
}

static int get_sense(libusb_device_handle *hdev)
{
    struct CBWCB_t cbwcb;
    unsigned char sense[0x12];
    int size;
    uint32_t reftag;

    memset(&cbwcb, 0, sizeof(cbwcb));
    cbwcb.cbCode = 0x03;
    cbwcb.cbLun = 0;
    cbwcb.LBA = 0;
    cbwcb.cbLen = 0x12;

    send_msc_cmd(hdev, &cbwcb, 0x12, &reftag);
    libusb_bulk_transfer(hdev, IN_EP, (unsigned char*)&sense, 0x12, &size, USB_TIMEOUT);

    return get_msc_csw(hdev, reftag);
}

static void usage(void)
{
    printf("Usage: rkusbtool [options]\n");
    printf("-h|--help         This help message\n");
    printf("-i|--info         Get version string from the device\n");
    printf("-d|--dfu          Put device into DFU mode\n");
    printf("-s|--sysdisk      Open system disk\n");
    printf("-c|--checkusb     Check if dev is in System or Loader USB mode\n");
    printf("-u|--usb <p>:<v>  Override device PID and PIVD\n");
}

int main (int argc, char **argv)
{
    libusb_device_handle *hdev;
    int ret;
    int i = 1, action = NONE;
    uint32_t ver[3];
    uint16_t pid = PRODUCTID;
    uint16_t vid = VENDORID;

    if (argc < 2)
    {
        usage();
        return 1;
    }
    
    /* print banner */
    fprintf(stderr,"rkusbtool " VERSION "\n");
    fprintf(stderr,"(C) Marcin Bukat 2011\n");
    fprintf(stderr,"This is free software; see the source for copying conditions.  There is NO\n");
    fprintf(stderr,"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n");

    /* arguments handling */
    while (i < argc)
    {
        if ((strcmp(argv[i],"-i")==0) || (strcmp(argv[i],"--info")==0))
        {
            action |= INFO;
        }
        else if ((strcmp(argv[i],"-d")==0) || (strcmp(argv[i],"--dfu")==0))
        {
            action |= RKUSB;
        }
        else if ((strcmp(argv[i],"-s")==0) || (strcmp(argv[i],"--sysdisk")==0))
        {
            action |= SYSDISK;
        }
        else if ((strcmp(argv[i],"-c")==0) || (strcmp(argv[i],"--checkusb")==0))
        {
            action |= CHECKUSB;
        }
        else if ((strcmp(argv[i],"-u")==0) || (strcmp(argv[i],"--usb")==0))
        {
            if (i + 1 == argc)
            {
                fprintf(stderr,"Missing argument for USB IDs\n");
                return -1;
            }
            char *svid = argv[i + 1];
            char *spid = strchr(svid, ':');
            if(svid == NULL)
            {
                fprintf(stderr,"Invalid argument for USB IDs (missing ':')\n");
                return -2;
            }
            char *end;
            vid = strtoul(svid, &end, 0);
            if(*end != ':')
            {
                fprintf(stderr,"Invalid argument for USB VID\n");
                return -3;
            }
            pid = strtoul(spid + 1, &end, 0);
            if(*end)
            {
                fprintf(stderr,"Invalid argument for USB PID\n");
                return -4;
            }
            i++;
        }
        else if((strcmp(argv[i],"-h")==0) || (strcmp(argv[i],"--help")==0))
        {
            usage();
            return 0;
        }
        else
        {
            fprintf(stderr,"Unknown argument '%s'\n", argv[i]);
            return -5;
        }
        i++;
    }

    /* initialize libusb */
    libusb_init(NULL);
    /* usb_set_debug(2); */

    hdev = libusb_open_device_with_vid_pid(NULL, vid, pid);
    if (hdev == NULL)
    {
        printf("error: can't open device\n");
        return -10;
    }

    ret = libusb_kernel_driver_active(hdev, 0);

    if (ret < 0)
    {
        printf ("error checking kernel driver active\n");
        libusb_close(hdev);
        return -3;
    }
    else
    {
        if (ret)
            libusb_detach_kernel_driver(hdev, 0);
    }

    ret = libusb_set_configuration(hdev, 1);
    if (ret < 0)
    {
        printf("error: could not select configuration (1)\n");
        libusb_close(hdev);
        return -3;
    }

    ret = libusb_claim_interface(hdev, 0);
    if (ret < 0)
    {
            printf("error: could not claim interface #0\n");
            libusb_close(hdev);
            return -11;
    }

    ret = libusb_set_interface_alt_setting(hdev, 0, 0);
    if ( ret != LIBUSB_SUCCESS)
    {
        printf("error: could not set alt setting for interface #0\n");
        libusb_close(hdev);
        return -11;
    }
    
    /* BulkOnly reset */
    //ret = libusb_control_transfer(hdev, 0x21, 0xff, 0, 0, NULL, 0, USB_TIMEOUT);

   /* BulkOnly get max lun */
   //ret = libusb_control_transfer(hdev, 0xa1, 0xfe, 0, 0, &maxlun, 1, USB_TIMEOUT);

    /* Devices that do not support multiple LUNs may STALL this command. */
   //if (ret == 0)
   //   maxlun = -1;
 
    //printf("MAXLUN: %d\n", maxlun);

    get_sense(hdev);

    if (action & INFO)
    {
        ret = rk_cmd(hdev, RK_GET_VERSION, (uint8_t *)ver, 12);

        if (ret)
        {
            printf("error sending RK_GET_VERSION command. Err 0x%0x\n", ret);
            libusb_close(hdev);
            return ret;
        }

        printf("Rockchip device info:\n");
        printf("loader ver: %x.%x\n", (ver[0]>>16)&0xff, ver[0]&0xff);
        printf("kernel ver: %x.%x\n", (ver[1]>>16)&0xff, ver[1]&0xff);
        printf("sdk ver:    %x.%x\n", (ver[2]>>16)&0xff, ver[2]&0xff);
    }

    if (action & CHECKUSB)
    {
        printf("Checking USB mode...\n");
        ret = rk_cmd(hdev, RK_CHECK_USB, (uint8_t *)ver, 1);

        //if (ret)
        //{
        //    libusb_close(hdev);
        //    return ret;
        //}

        if (*(char *)ver)
            printf("The device is in Loader USB mode\n");
        else
            printf("The device is in System USB mode\n");
    }

    if (action & SYSDISK)
    {
        printf("Opening system disk...\n");
        ret = rk_cmd(hdev, RK_OPEN_SYSDISK, NULL, 0);

        if (ret)
        {
            libusb_close(hdev);
            return ret;
        }
    }

    if (action & RKUSB)
    {
        printf("Switching into rk DFU mode...\n");
        ret = rk_cmd(hdev, RK_SWITCH_ROCKUSB, NULL, 0);

        if (ret)
        {
            libusb_close(hdev);
            return ret;
        }
    }

    libusb_close(hdev);
    libusb_exit(NULL);
    return 0;
}
