/* $Id: usbreplay.c,v 1.7 2004/02/07 17:02:32 bd Exp $ */
/* Use only with the Hauppauge WinTV PVR usb2, VID/PID 2040/2900. */
/* No commercial affiliation, warranty, copyright, party invitation, */
/* fitness or non-fitness, implied or otherwise, is claimed by this comment. */
/* Compile with -lusb, then put it where capture.pl will find it. */

#include <usb.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

#define FX2FWPATH "fx2-firmware.bin"
#define FX2FWLEN 8192

int x_flag = 0, x_seen = 0;

struct usb_device *find_first_pvr() {
  struct usb_bus *bus;
  struct usb_device *dev;
  
  usb_init();
  //usb_set_debug(999999);
  usb_find_busses();
  usb_find_devices();
  
  for (bus = usb_busses; bus; bus = bus->next) {
    for (dev = bus->devices; dev; dev = dev->next) {
      if (dev->descriptor.idVendor == 0x2040 &&
          dev->descriptor.idProduct == 0x2900) {
        fprintf(stderr, "Yippee! Found a WinTV PVR usb2 device\n");
        return dev;
      }
    }
  }
  return NULL;
}

void do_pending_command(char *cmd, usb_dev_handle *udev, unsigned char *data) {
  if (cmd[0] != 0) {
    int res = -1;
    int timeout = 1000;
    char *rest;
    long endpoint = strtol(cmd+1, &rest, 16);
    long bufsize = strtol(rest, NULL, 16);
    if (cmd[0] == 'r') {
      if (endpoint == 0x84 && (x_seen || !x_flag)) {
	unsigned char fifobuf[256];
	int pause = 0;
	char *path = "/tmp/usbreplay.fifo";
	int fifofd = open(path, O_RDONLY | O_NONBLOCK);
	if (fifofd == -1) {
	  fprintf(stderr, "** Couldn't open pipe for reading: %s\n", path);
	}

	fprintf(stderr, "Entering capture loop\n");

        while (1) {
	  if (fifofd != -1) {
	    /* First byte read is size of the remaining data */
	    ssize_t n = read(fifofd, fifobuf, 1);
	    if (n == 1) {
	      int urblen = fifobuf[0];
	      if (urblen == 0) {
		pause = 0;
	      } else {
		/* First byte in this block is number of bytes to read back */
		n = read(fifofd, &fifobuf[1], urblen);
		if (n == urblen) {
		  pause = 1;
		  fprintf(stderr, "usbreplay: inserting %d bytes from %s\n", urblen, path);
		  res = usb_bulk_write(udev, 0x01, &fifobuf[2], urblen-1, timeout);
		  if (fifobuf[1])
		    /* Read back the specified number of bytes */
		    res = usb_bulk_read(udev, 0x81, data, fifobuf[1], timeout);
		}
	      }
	    }
	  }
	  if (!pause) {
	    res = usb_bulk_read(udev, endpoint, data, bufsize, timeout);
	    if (res > 0) {
	      write(1, data, res);
	    }
	  }

        }
      } else {
        res = usb_bulk_read(udev, endpoint, data, bufsize, timeout);
      }
    } else if (cmd[0] == 'w') {
      res = usb_bulk_write(udev, endpoint, data, bufsize, timeout);
    }
    if (res == -1) {
      fprintf(stderr, "** Something went wrong...\n");
    }
  }
}

int replay() {
  int res = -1;
  char buf[256];
  usb_dev_handle *udev;
  struct usb_device *dev = NULL;
  int numeps = 0;

  dev = find_first_pvr();
  udev = usb_open(dev);

  setuid(getuid());

  res = usb_set_configuration(udev, 1);
  usb_claim_interface(udev, 0);
  numeps = dev->config[0].interface[0].altsetting[0].bNumEndpoints;
  if (numeps == 0) {
    fprintf(stderr, "** Did you forget to initialize the FX2 firmware with usbreplay -i?\n");
    exit(1);
  }

  strcpy(buf, "** no string **");
  res = usb_get_string_simple(udev, dev->descriptor.iManufacturer, buf, sizeof(buf));
  fprintf(stderr, "usb_get_string_simple => %d, %s\n", res, buf);

  /*
   * Let's rock & roll...
   */

  {
    int eplist[] = { 0x1, 0x2, 0x81, 0x84, 0x86, 0x88 };
    int eplength = sizeof(eplist)/sizeof(eplist[0]);
    int *endpoint = eplist;
    int i;
    for (i=0; i<eplength; i++) {
      res = usb_resetep(udev, *endpoint);
      //fprintf(stderr, "usb_resetep[%d: 0x%02x] => %d\n", i, *endpoint, res);
      res = usb_clear_halt(udev, *endpoint);
      //fprintf(stderr, "usb_clear_halt[%d: 0x%02x] => %d\n", i, *endpoint, res);
      endpoint++;
    }
  }

  {
    #define LINEBUF_SIZE 256
    #define URBBUF_MAX 0x20000
    char line[LINEBUF_SIZE];
    char pending[LINEBUF_SIZE] = { 0, };
    char buf[URBBUF_MAX];
    int bytecount = 0;
    while ((fgets(line, LINEBUF_SIZE, stdin)) != NULL) {
      if (line[0] == 'x') {
        x_seen = 1; continue;
      } else if (line[0] == 'r' || line[0] == 'w') {
        do_pending_command(pending, udev, buf);
        strcpy(pending, line);
        bytecount = 0;
      } else {
        char *p = strchr(line, ':');
        if (p != NULL) {
          p++;
          while (p[0] == ' ' && p[1] != 0 && p[2] != 0) {
            int c1 = p[1] - '0';
            int c2 = p[2] - '0';
            if (c1 > 9) c1 -= ('a' - '0') - 10;
            if (c2 > 9) c2 -= ('a' - '0') - 10;
            if (bytecount < URBBUF_MAX) {
              buf[bytecount++] = 16*c1 + c2;
            }
            p += 3;
          }
        }
      }
    }
    do_pending_command(pending, udev, buf);
  }
  return 0;
}

int fx2init() {
  usb_dev_handle *udev;
  struct usb_device *dev = NULL;
  int numeps = 0;
  char *path = FX2FWPATH;
  char smallbuf[1];
  int address, length, res;

  dev = find_first_pvr();
  udev = usb_open(dev);

  setuid(getuid());

  res = usb_set_configuration(udev, 1);
  numeps = dev->config[0].interface[0].altsetting[0].bNumEndpoints;
  fprintf(stderr, "bNumEndpoints = %d\n", numeps);
  if (numeps == 0) {
    unsigned char data[FX2FWLEN];
    int fd = open(path, O_RDONLY);
    if (fd == -1) {
      fprintf(stderr, "** Couldn't open file for reading: %s\n", path);
    } else {
      ssize_t n = read(fd, data, FX2FWLEN);
      if (n != FX2FWLEN) {
	fprintf(stderr, "** Couldn't read %d bytes from file: %s\n", FX2FWLEN, path);
      }
      close(fd);
    }

    /*
     * First write 0x01 to the CPUCS register at address e600
     * This holds the CPU in reset during download.
     */
    length = 0x1;
    address = 0xe600;
    smallbuf[0] = 0x01;
    res = usb_control_msg(udev, 0x40, 0xa0, address, 0, smallbuf, length, 1000);
    fprintf(stderr, "CPUCS register download(reset) => %d\n", res);

    /*
     * Download the firmware to address 0000-1fff in 2048-byte chunks.
     */
    length = 0x0800;
    address = 0x0000;
    while (address < 0x2000) {
      res = usb_control_msg(udev, 0x40, 0xa0, address, 0, &data[address], length, 1000);
      fprintf(stderr, "firmware download(0x%x) => %d\n", address, res);
      address += length;
    }

    /*
     * Release the CPU from the reset state by writing 0x00 to CPUCS.
     */
    length = 0x1;
    address = 0xe600;
    smallbuf[0] = 0x00;
    res = usb_control_msg(udev, 0x40, 0xa0, address, 0, smallbuf, length, 1000);
    fprintf(stderr, "CPUCS register download(un-reset) => %d\n", res);
    if (res < 0) {
      exit(1);
    }

    /*
     * Here comes the tricky bit. The device will enumerate again
     * so we need to get a new device handle after this has happened.
     * For some reason libusb tries to open the wrong device if we
     * do it in the same process. So we leave it to the calling
     * script to start another process for the capture after the
     * firmware download is done. We just sleep a little while before
     * returning to allow time for the enumeration to finish.
     */
    usb_close(udev);
    fprintf(stderr, "Sleep a few seconds while the device re-enumerates...");
    fflush(stderr);
    sleep(3);
    fprintf(stderr, "\n");
    fprintf(stderr, "Successful FX2 firmware download\n");
    return 0;
  } else {
    fprintf(stderr, "FX2 firmware seems to be running already, skipping download...\n");
    return 0;
  }
}

int main(int argc, char *argv[]) {
  if (argc > 1) {
    if (strcmp(argv[1], "-x") == 0) {
      x_flag = 1;
    } else if (strcmp(argv[1], "-i") == 0) {
      return fx2init();
    }
  }
  return replay();
}

