/*
 * dfu-programmer
 *
 * $Id: dfu.c,v 1.3 2006/06/20 06:28:04 schmidtw Exp $
 *
 * This program 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 program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <stdio.h>
#include <usb.h>
#include "dfu.h"

/* DFU commands */
#define DFU_DETACH      0
#define DFU_DNLOAD      1
#define DFU_UPLOAD      2
#define DFU_GETSTATUS   3
#define DFU_CLRSTATUS   4
#define DFU_GETSTATE    5
#define DFU_ABORT       6

#define INVALID_DFU_TIMEOUT -1

static int dfu_timeout = INVALID_DFU_TIMEOUT;
static unsigned short transaction = 0;

static int dfu_debug_level = 0;

void dfu_init( const int timeout )
{
    if( timeout > 0 ) {
        dfu_timeout = timeout;
    } else {
        if( 0 != dfu_debug_level )
            fprintf( stderr, "dfu_init: Invalid timeout value.\n" );
    }
}

static int dfu_verify_init( const char *function )
{
    if( INVALID_DFU_TIMEOUT == dfu_timeout ) {
        if( 0 != dfu_debug_level )
            fprintf( stderr,
                     "%s: dfu system not property initialized.\n",
                     function );
        return -1;
    }

    return 0;
}

void dfu_debug( const int level )
{
    dfu_debug_level = level;
}


/*
 *  DFU_DETACH Request (DFU Spec 1.0, Section 5.1)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *  timeout   - the timeout in ms the USB device should wait for a pending
 *              USB reset before giving up and terminating the operation
 *
 *  returns 0 or < 0 on error
 */
int dfu_detach( struct usb_dev_handle *device,
                const unsigned short interface,
                const unsigned short timeout )
{
    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    return usb_control_msg( device,
        /* bmRequestType */ USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
        /* bRequest      */ DFU_DETACH,
        /* wValue        */ timeout,
        /* wIndex        */ interface,
        /* Data          */ NULL,
        /* wLength       */ 0,
                            dfu_timeout );
}


/*
 *  DFU_DNLOAD Request (DFU Spec 1.0, Section 6.1.1)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *  length    - the total number of bytes to transfer to the USB
 *              device - must be less than wTransferSize
 *  data      - the data to transfer
 *
 *  returns the number of bytes written or < 0 on error
 */
int dfu_download( struct usb_dev_handle *device,
                  const unsigned short interface,
                  const unsigned short length,
                  char* data )
{
    int status;

    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    /* Sanity checks */
    if( (0 != length) && (NULL == data) ) {
        if( 0 != dfu_debug_level )
            fprintf( stderr,
                     "%s: data was NULL, but length != 0\n",
                     __FUNCTION__ );
        return -1;
    }

    if( (0 == length) && (NULL != data) ) {
        if( 0 != dfu_debug_level )
            fprintf( stderr,
                     "%s: data was not NULL, but length == 0\n",
                     __FUNCTION__ );
        return -2;
    }

    status = usb_control_msg( device,
          /* bmRequestType */ USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
          /* bRequest      */ DFU_DNLOAD,
          /* wValue        */ transaction++,
          /* wIndex        */ interface,
          /* Data          */ data,
          /* wLength       */ length,
                              dfu_timeout );
    if( status < 0 ) {
        fprintf( stderr, "%s error %d\n", __FUNCTION__, status );
    }

    return status;
}


/*
 *  DFU_UPLOAD Request (DFU Spec 1.0, Section 6.2)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *  length    - the maximum number of bytes to receive from the USB
 *              device - must be less than wTransferSize
 *  data      - the buffer to put the received data in
 *
 *  returns the number of bytes received or < 0 on error
 */
int dfu_upload( struct usb_dev_handle *device,
                const unsigned short interface,
                const unsigned short length,
                char* data )
{
    int status;

    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    /* Sanity checks */
    if( (0 == length) || (NULL == data) ) {
        if( 0 != dfu_debug_level )
            fprintf( stderr,
                     "%s: data was NULL, or length is 0\n",
                     __FUNCTION__ );
        return -1;
    }

    status = usb_control_msg( device,
          /* bmRequestType */ USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
          /* bRequest      */ DFU_UPLOAD,
          /* wValue        */ transaction++,
          /* wIndex        */ interface,
          /* Data          */ data,
          /* wLength       */ length,
                              dfu_timeout );
    if( status < 0 ) {
        fprintf( stderr, "%s error %d\n", __FUNCTION__, status );
    }

    return status;
}


/*
 *  DFU_GETSTATUS Request (DFU Spec 1.0, Section 6.1.2)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *  status    - the data structure to be populated with the results
 *
 *  return the number of bytes read in or < 0 on an error
 */
int dfu_get_status( struct usb_dev_handle *device,
                    const unsigned short interface,
                    struct dfu_status *status )
{
    char buffer[6];
    int result;

    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    /* Initialize the status data structure */
    status->bStatus       = DFU_STATUS_ERROR_UNKNOWN;
    status->bwPollTimeout = 0;
    status->bState        = STATE_DFU_ERROR;
    status->iString       = 0;

    result = usb_control_msg( device,
          /* bmRequestType */ USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
          /* bRequest      */ DFU_GETSTATUS,
          /* wValue        */ 0,
          /* wIndex        */ interface,
          /* Data          */ buffer,
          /* wLength       */ 6,
                              dfu_timeout );

    if( 6 == result ) {
        status->bStatus = buffer[0];
        status->bwPollTimeout = ((0xff & buffer[3]) << 16) |
                                ((0xff & buffer[2]) << 8)  |
                                (0xff & buffer[1]);

        status->bState  = buffer[4];
        status->iString = buffer[5];
    }

    return result;
}


/*
 *  DFU_CLRSTATUS Request (DFU Spec 1.0, Section 6.1.3)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *
 *  return 0 or < 0 on an error
 */
int dfu_clear_status( struct usb_dev_handle *device,
                      const unsigned short interface )
{
    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    return usb_control_msg( device,
        /* bmRequestType */ USB_ENDPOINT_OUT| USB_TYPE_CLASS | USB_RECIP_INTERFACE,
        /* bRequest      */ DFU_CLRSTATUS,
        /* wValue        */ 0,
        /* wIndex        */ interface,
        /* Data          */ NULL,
        /* wLength       */ 0,
                            dfu_timeout );
}


/*
 *  DFU_GETSTATE Request (DFU Spec 1.0, Section 6.1.5)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *  length    - the maximum number of bytes to receive from the USB
 *              device - must be less than wTransferSize
 *  data      - the buffer to put the received data in
 *
 *  returns the state or < 0 on error
 */
int dfu_get_state( struct usb_dev_handle *device,
                   const unsigned short interface )
{
    int result;
    char buffer[1];

    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    result = usb_control_msg( device,
          /* bmRequestType */ USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
          /* bRequest      */ DFU_GETSTATE,
          /* wValue        */ 0,
          /* wIndex        */ interface,
          /* Data          */ buffer,
          /* wLength       */ 1,
                              dfu_timeout );

    /* Return the error if there is one. */
    if( result < 1 ) {
        return result;
    }

    /* Return the state. */
    return buffer[0];
}


/*
 *  DFU_ABORT Request (DFU Spec 1.0, Section 6.1.4)
 *
 *  device    - the usb_dev_handle to communicate with
 *  interface - the interface to communicate with
 *
 *  returns 0 or < 0 on an error
 */
int dfu_abort( struct usb_dev_handle *device,
               const unsigned short interface )
{
    if( 0 != dfu_verify_init(__FUNCTION__) )
        return -1;

    return usb_control_msg( device,
        /* bmRequestType */ USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
        /* bRequest      */ DFU_ABORT,
        /* wValue        */ 0,
        /* wIndex        */ interface,
        /* Data          */ NULL,
        /* wLength       */ 0,
                            dfu_timeout );
}


char* dfu_state_to_string( int state )
{
    char *message = NULL;

    switch( state ) {
        case STATE_APP_IDLE:
            message = "appIDLE";
            break;
        case STATE_APP_DETACH:
            message = "appDETACH";
            break;
        case STATE_DFU_IDLE:
            message = "dfuIDLE";
            break;
        case STATE_DFU_DOWNLOAD_SYNC:
            message = "dfuDNLOAD-SYNC";
            break;
        case STATE_DFU_DOWNLOAD_BUSY:
            message = "dfuDNBUSY";
            break;
        case STATE_DFU_DOWNLOAD_IDLE:
            message = "dfuDNLOAD-IDLE";
            break;
        case STATE_DFU_MANIFEST_SYNC:
            message = "dfuMANIFEST-SYNC";
            break;
        case STATE_DFU_MANIFEST:
            message = "dfuMANIFEST";
            break;
        case STATE_DFU_MANIFEST_WAIT_RESET:
            message = "dfuMANIFEST-WAIT-RESET";
            break;
        case STATE_DFU_UPLOAD_IDLE:
            message = "dfuUPLOAD-IDLE";
            break;
        case STATE_DFU_ERROR:
            message = "dfuERROR";
            break;
    }

    return message;
}

/* Chapter 6.1.2 */
static const char *dfu_status_names[] = {
	[DFU_STATUS_OK]			= "No error condition is present",
	[DFU_STATUS_errTARGET]		= 
		"File is not targeted for use by this device",
	[DFU_STATUS_errFILE]		=
		"File is for this device but fails some vendor-specific test",
	[DFU_STATUS_errWRITE]		=
		"Device is unable to write memory",
	[DFU_STATUS_errERASE]		=
		"Memory erase function failed",
	[DFU_STATUS_errCHECK_ERASED]	=
		"Memory erase check failed",
	[DFU_STATUS_errPROG]		=
		"Program memory function failed",
	[DFU_STATUS_errVERIFY]		=
		"Programmed emmory failed verification",
	[DFU_STATUS_errADDRESS]		=
		"Cannot program memory due to received address that is out of range",
	[DFU_STATUS_errNOTDONE]		=
		"Received DFU_DNLOAD with wLength = 0, but device does not think that it has all data yet",
	[DFU_STATUS_errFIRMWARE]	=
		"Device's firmware is corrupt. It cannot return to run-time (non-DFU) operations",
	[DFU_STATUS_errVENDOR]		=
		"iString indicates a vendor specific error",
	[DFU_STATUS_errUSBR]		=
		"Device detected unexpected USB reset signalling",
	[DFU_STATUS_errPOR]		=
		"Device detected unexpected power on reset",
	[DFU_STATUS_errUNKNOWN]		=
		"Something went wrong, but the device does not know what it was",
	[DFU_STATUS_errSTALLEDPKT]	=
		"Device stalled an unexpected request",
};


const char *dfu_status_to_string(int status)
{
	if (status > DFU_STATUS_errSTALLEDPKT)
		return "INVALID";
	return dfu_status_names[status];
}

